Compare commits

..

14 Commits

Author SHA1 Message Date
obel1x f4e5de2f5a Merge pull request 'install.sh: free space detection, btrfs online resize, sfdisk, partition naming + install.md fix' (#14) from unbrot/fedora-OEMDRV:main into devel
Reviewed-on: #14
2026-04-27 17:26:03 +02:00
unbrot 1f17ef5c2f Update install.md: download script before running with sudo
Process substitution does not survive sudo, so the script must be
downloaded to a temp file first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 17:15:06 +02:00
unbrot 296026b667 Set GPT partition name to anacondainstall for OEMDRV partition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:45:16 +02:00
unbrot 5ef48c84e5 Replace parted resizepart with sfdisk --force
parted fails to resize a mounted partition non-interactively.
sfdisk --no-reread --force writes the updated partition table
directly without TTY/confirmation issues, and partprobe
re-reads it afterwards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:40:18 +02:00
unbrot 5c2182a1c9 Fix parted failing on busy partition in script mode
parted -s answers confirmation prompts with "no" (conservative),
causing resizepart/mkpart to fail on a mounted partition. Pipe
"Yes" to parted stdin instead so busy-partition warnings are
confirmed and the operation proceeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:30:19 +02:00
unbrot bf71540f66 Fix btrfs resize: use online resize instead of unmount/remount
btrfs supports live filesystem resize, so there is no need to
unmount a mounted btrfs partition before shrinking it. This also
avoids umount failures when the partition is busy (e.g. /home with
an active SSH session). ext4 still requires offline resize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:26:27 +02:00
unbrot bfa5515d75 Fix double /dev/ prefix bug in collect_partitions
lsblk -p returns PKNAME as a full path (/dev/sda), so stripping
the basename before prepending /dev/ avoids /dev//dev/sda.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:23:51 +02:00
unbrot 9e16162077 Add free space detection to install.sh as alternative to partition shrinking
If a disk has unpartitioned space >= 4096 MiB, it is offered as a direct
target for the OEMDRV partition, avoiding any filesystem resize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 14:59:14 +02:00
unbrot 9974facd45 Fix install.sh exiting immediately when not run as root
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 14:07:02 +02:00
unbrot 4d9ec0d356 Moved install-md to top level 2026-04-27 13:45:55 +02:00
unbrot 6bb7cac7ad Add install.md with usage instructions for install.sh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:23:55 +02:00
unbrot 5d0c674bec Add install.sh bootstrap script to create OEMDRV partition
Shrinks a selected ext4/btrfs partition by 4 GiB, creates a new BTRFS
partition labeled OEMDRV, mounts it to /opt/sys_config and clones the
repository into it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:14:41 +02:00
unbrot 576363cdca Fix Nextcloud app password not stored in KWallet after autoprovisioning
The Flatpak autoprovisioning command does not reliably write credentials
to KWallet from inside the sandbox. After provisioning, directly write
both KWallet entries (user:url/:0 and user_app-password:url/:0) via
qdbus, creating the Nextcloud folder first if needed. kwallet-query was
tried but silently returns 0 without creating missing folders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:30:49 +02:00
unbrot 5620e73c4d Add CLAUDE.md with codebase guidance for Claude Code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:54:47 +02:00
4 changed files with 618 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A Fedora automated mass-installation and post-setup scripting collection. It uses an **OEMDRV** partition (BTRFS, mounted at `/opt/sys_config`) that Anaconda/Kickstart picks up automatically during Fedora installation. The system requires:
- A FreeIPA server (domain controller, KRA vault for encryption keys)
- A Nextcloud instance (config and software sync, WebDAV token auth)
- Client PCs with an OEMDRV partition prepared beforehand
## Configuration
Before any script runs, copy the dist file and fill in your environment:
```sh
cp /opt/sys_config/config/setup_system.conf.dist /opt/sys_config/config/setup_system.conf
# Edit setup_system.conf with your domain, server FQDNs, paths, etc.
```
Local per-machine overrides go in `config.d/*.conf` (gitignored). These are sourced after `setup_system.conf` and can override any exported variable (e.g. `config.d/system_defines.conf` overrides `UPGRADEBRANCH`).
`config/skel.tar.zst` (gitignored) holds the `/etc/skel` archive deployed to new installs. The `.dist` version is the default. To modify skel: extract, edit, then repack:
```sh
cd /opt/sys_config/config
tar -I 'zstd -9' -cf skel.tar.zst skel/ # or use pack_skel.sh
```
## Installation lifecycle
1. **Kickstart pre**`ks_base_profiles/basic_pre_script.inc` runs inside Anaconda's `%pre` section. It locates the OEMDRV partition, identifies the target drive, and **deletes all other partitions on that drive** (non-interactive, no prompt).
2. **Kickstart post** — After Fedora installs, `setup_system_full.sh install` runs (as root, non-interactive). It adjusts umask, adds the OEMDRV fstab entry, deploys `/etc/skel` via `setup_skel.sh`, and installs the `setup-system.service` systemd unit.
3. **First boot** (`firstrun_run` mode) — The `setup-system.service` unit runs `setup_system_full.sh firstrun_run` on `/dev/tty2`. It calls `ipa_register_host` which prompts for domain credentials and joins the PC to FreeIPA via `ipa-client-install`. The service then disables itself.
4. **User logon**`logon_script.sh` is triggered by KDE autostart. It:
- Mounts the gocryptfs-encrypted home directory (`mount_ecrypt_home.sh`) using a key stored in the FreeIPA KRA vault (`IPAVAULTNAME`)
- Obtains a Nextcloud WebDAV app token (`get_nc_token` in `setup_system.inc.sh`)
- Checks for a matching IPA sudo rule, then calls `sync_client_software.sh install` as root (preserving env) to sync configs and run software installs
- Calls `client_software/user_run.sh` (as the logged-in user)
- Syncs Firefox and Thunderbird profiles via Nextcloud (`mozilla_starter.sh`)
## Script roles
| Script | Who runs it | How called |
|---|---|---|
| `system_setup/setup_system.inc.sh` | sourced, never executed directly | `source`d by all other scripts |
| `system_setup/setup_system_full.sh` | root | kickstart post, firstrun service, or manual |
| `system_setup/logon_script.sh` | domain user | KDE autostart (via `.desktop` in autostart) |
| `system_setup/sync_client_software.sh` | root (sudo, preserve-env) | called by logon_script.sh |
| `system_setup/mount_ecrypt_home.sh` | user | called by logon_script.sh |
| `system_setup/mozilla_starter.sh` | user | called by logon_script.sh; args: `firefox\|thunderbird run\|sync [profile]` |
| `system_setup/setup_skel.sh` | root | called by setup_system_full.sh or manually |
| `config/pack_skel.sh` | root | manually, to repack skel archive after editing |
| `system_setup/create_nc_package_from_sys_config.sh` | user | manually, creates `~/temp/sys_config.tar.zst` |
## client_software layout
`client_software/` is synced from Nextcloud (`CLIENT_SOFTWARE_SRC``CLIENT_SOFTWARE_DST`). Each numbered subdirectory may contain:
- `install.sh` — run as root by `client_software/install.sh` (iterates sorted dirs)
- `user_run.sh` — run as the logged-in user by `client_software/user_run.sh`
Naming convention: directories `< 0100` are base installs, `>= 0100` are additional apps. Pass a filter string to run only matching directories:
```sh
# Run only the kwallet install:
${CLIENT_SOFTWARE_DST}/install.sh 0010_kwallet
```
## Kickstart files
- `ks.cfg` — the primary kickstart used for production installs (Fedora 43, KDE, x86_64, German locale/keyboard)
- `ks_base_profiles/kde_fullsetup.cfg` — an alternate/reference profile generated by Anaconda
- `ks_base_profiles/minimal_setup.cfg`, `part_sda.cfg` — additional profile fragments
- `ks_pc_prof/` — per-machine kickstart overrides, named by system UUID suffix (e.g. `pc-9cdb93ef7c20.cfg`)
## Sudo rule required for logon_script
The logon script requires a FreeIPA sudo rule allowing the domain user group to run `sync_client_software.sh` as root without a password, with environment preservation. The rule must include `!authenticate` and `setenv` options. The expected command pattern:
```
^/opt/sys_config/system_setup/sync_client_software\.sh.*$
```
## gitignore notes
The following are intentionally excluded from git and must be set up locally:
- `config/setup_system.conf` — site-specific config (copy from `.dist`)
- `config/skel.tar.zst` — skel archive (copy from `.dist` or rebuild)
- `config.d/*.conf` — local overrides
- `client_software/.sync_*.db` — Nextcloud sync DB files
@@ -93,6 +93,35 @@ for i in {0..99}; do
echo "Please check the above output!"
exit 1
fi
# The Flatpak autoprovisioning may not successfully write the apppassword to
# KWallet from inside the sandbox, so write it directly via D-Bus.
# Nextcloud stores HTTP credentials in folder "Nextcloud" with keys:
# user:url/:0 (legacy password entry)
# user_app-password:url/:0 (app password entry, used for auth)
NC_WALLET_URL="https://${SERVERFQDN_NC}/"
NC_WALLET_APPID="logon_script"
NC_QB_CMD="qdbus-qt6"
if ! command -v ${NC_QB_CMD} >/dev/null 2>&1; then NC_QB_CMD="qdbus"; fi
NC_QB_SVC="org.kde.kwalletd"
NC_QB_PATH="/modules/kwalletd6"
if ! ( ${NC_QB_CMD} "${NC_QB_SVC}" | grep -q "${NC_QB_PATH}" ); then
NC_QB_PATH="/modules/kwalletd5"
fi
echo "Writing Nextcloud app password to KWallet via D-Bus (${NC_QB_PATH})"
NC_WALLET_HANDLE=$(${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.open "kdewallet" 0 "${NC_WALLET_APPID}")
if [[ -n "${NC_WALLET_HANDLE}" && "${NC_WALLET_HANDLE}" != "-1" ]]; then
HAS_FOLDER=$(${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.hasFolder "${NC_WALLET_HANDLE}" "Nextcloud" "${NC_WALLET_APPID}")
if [[ "${HAS_FOLDER}" != "true" ]]; then
${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.createFolder "${NC_WALLET_HANDLE}" "Nextcloud" "${NC_WALLET_APPID}" >/dev/null
fi
${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.writePassword "${NC_WALLET_HANDLE}" "Nextcloud" "${DAVTOKEN_USER}:${NC_WALLET_URL}:0" "${DAVTOKEN_PASS}" "${NC_WALLET_APPID}" >/dev/null
${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.writePassword "${NC_WALLET_HANDLE}" "Nextcloud" "${DAVTOKEN_USER}_app-password:${NC_WALLET_URL}:0" "${DAVTOKEN_PASS}" "${NC_WALLET_APPID}" >/dev/null
${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.sync "${NC_WALLET_HANDLE}" "${NC_WALLET_APPID}" >/dev/null
${NC_QB_CMD} ${NC_QB_SVC} ${NC_QB_PATH} org.kde.KWallet.close "${NC_WALLET_HANDLE}" false "${NC_WALLET_APPID}" >/dev/null
echo "Nextcloud app password written to KWallet successfully."
else
echo "Warning: Could not open KWallet (handle: ${NC_WALLET_HANDLE}). Nextcloud may prompt for credentials on next start."
fi
fi
done
+52
View File
@@ -0,0 +1,52 @@
# OEMDRV Bootstrap — install.sh
the script `./system_setup/install.sh` prepares a target machine for automated Fedora deployment. It shrinks an existing partition to carve out a dedicated **OEMDRV** partition, which Anaconda/Kickstart will detect automatically during installation.
## What it does
1. Lists all ext4 and btrfs partitions that have enough free space to be shrunk.
2. Asks you to select one and shrinks it by **4 GiB**.
3. Creates a new 4 GiB BTRFS partition labeled `OEMDRV` in the freed space.
4. Mounts it to `/opt/sys_config` with `compress=zstd:6`.
5. Clones this repository (depth 1) into `/opt/sys_config`.
## Prerequisites
- Run as **root** on the target machine (live system or installed OS).
- The following tools must be present: `parted`, `e2fsck`, `resize2fs` or `btrfs-progs`, `mkfs.btrfs`, `git`, `curl`.
- The partition you want to shrink must **not** be the root filesystem (`/`) and must have at least **4.5 GiB free**.
- Network access to `gitea.dtext.online`.
## Run directly from the repository
Download the script first, then run it as root:
```bash
curl -fsSL https://gitea.dtext.online/obel1x/fedora-OEMDRV/raw/branch/main/system_setup/install.sh -o /tmp/install.sh
sudo bash /tmp/install.sh
```
## After the script completes
Configure your environment before running any installation:
```sh
cp /opt/sys_config/config/setup_system.conf.dist /opt/sys_config/config/setup_system.conf
# Edit setup_system.conf — set TLDOMAIN, SERVERFQDN_IPA, SERVERFQDN_NC, and paths.
```
Optionally add local per-machine overrides in `config.d/`:
```sh
# Example: use the devel branch on this machine
echo 'export UPGRADEBRANCH="devel"' > /opt/sys_config/config.d/system_defines.conf
```
Once configured, boot the Fedora installer from USB — Anaconda will detect the `OEMDRV` partition and run the Kickstart automatically.
## Supported filesystems for shrinking
| Filesystem | Method |
|---|---|
| ext4 | `e2fsck` + `resize2fs` (offline) |
| btrfs | `btrfs filesystem resize` (temporary mount) |
+448
View File
@@ -0,0 +1,448 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Daniel Pätzold
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Bootstrap script: creates an OEMDRV partition by shrinking an existing
# ext4 or btrfs partition by 4 GiB, formats the freed space as BTRFS with
# label OEMDRV, mounts it to /opt/sys_config and clones the repository.
#
# Run as root on a target machine before any Kickstart installation.
[[ "$EUID" -eq 0 ]] || { echo "ERROR: This script must be run as root." >&2; exit 1; }
SHRINK_MIB=4096
OEMDRV_LABEL="OEMDRV"
MOUNT_POINT="/opt/sys_config"
MOUNT_OPTS="compress=zstd:6"
REPO_URL="https://gitea.dtext.online/obel1x/fedora-OEMDRV.git"
MIN_FREE_MIB=$(( SHRINK_MIB + 512 )) # require 512 MiB headroom above the shrink size
# ── Helpers ───────────────────────────────────────────────────────────────────
die() { echo; echo "ERROR: $*" >&2; exit 1; }
info() { echo; echo ">>> $*"; }
hr() { printf '%.0s─' {1..100}; echo; }
require_root() {
[[ "$EUID" -eq 0 ]] || die "This script must be run as root."
}
check_tools() {
local missing=()
for tool in lsblk blkid parted partprobe mkfs.btrfs git e2fsck resize2fs tune2fs; do
command -v "$tool" >/dev/null 2>&1 || missing+=("$tool")
done
[[ ${#missing[@]} -eq 0 ]] || die "Missing required tools: ${missing[*]}"
}
# ── Free-space helpers ────────────────────────────────────────────────────────
# Free MiB for a mounted device via df
mounted_free_mib() {
df --output=avail -BM "$1" 2>/dev/null | awk 'NR==2{gsub("M",""); print $1+0}'
}
# Free MiB for an unmounted ext4 device via tune2fs (no mount needed)
ext4_free_mib() {
local dev="$1" fb bs
fb=$(tune2fs -l "$dev" 2>/dev/null | awk '/^Free blocks:/{print $3}')
bs=$(tune2fs -l "$dev" 2>/dev/null | awk '/^Block size:/{print $3}')
[[ -n "$fb" && -n "$bs" ]] || { echo 0; return; }
echo $(( fb * bs / 1048576 ))
}
# Free MiB for a btrfs device mounts temporarily if not already mounted
btrfs_free_mib() {
local dev="$1" mnt free tmp=0
mnt=$(lsblk -n -o MOUNTPOINT "$dev" 2>/dev/null | grep -v '^$' | head -1)
if [[ -z "$mnt" ]]; then
mnt=$(mktemp -d /tmp/oemdrv_check.XXXXXX)
mount -o ro "$dev" "$mnt" 2>/dev/null && tmp=1 || { rmdir "$mnt"; echo 0; return; }
fi
free=$(mounted_free_mib "$mnt")
[[ $tmp -eq 1 ]] && { umount "$mnt"; rmdir "$mnt"; }
echo "${free:-0}"
}
# ── Partition discovery ───────────────────────────────────────────────────────
# Parallel arrays, indexed 0..PT_IDX-1
PT_IDX=0
PT_DEV=() # /dev/sda2
PT_DISK=() # /dev/sda
PT_PNUM=() # 2
PT_SIZE=() # "100G"
PT_FS=() # ext4 | btrfs
PT_LABEL=() # home | -
PT_UUID=() # xxxxxxxx | -
PT_MNT=() # /home | -
PT_FREE=() # free MiB (integer)
PT_OK=() # 1 = shrinkable
# Parallel arrays for unpartitioned free space regions, indexed 0..FS_IDX-1
FS_IDX=0
FS_DISK=() # /dev/sda
FS_START=() # region start, MiB
FS_END=() # region end, MiB
FS_SIZE=() # region size, MiB
collect_partitions() {
local NAME SIZE FSTYPE LABEL UUID MOUNTPOINT TYPE PKNAME
while IFS= read -r line; do
NAME="" SIZE="" FSTYPE="" LABEL="" UUID="" MOUNTPOINT="" TYPE="" PKNAME=""
eval "$line"
[[ "$TYPE" != "part" ]] && continue
[[ "$FSTYPE" != "ext4" && "$FSTYPE" != "btrfs" ]] && continue
[[ "$LABEL" == "$OEMDRV_LABEL" ]] && continue
[[ -z "$PKNAME" ]] && continue
local disk="/dev/${PKNAME##*/}"
local pnum
pnum=$(cat /sys/class/block/"${NAME##/dev/}"/partition 2>/dev/null) || continue
# Free space check
local free_mib
if [[ -n "$MOUNTPOINT" ]]; then
free_mib=$(mounted_free_mib "$NAME")
elif [[ "$FSTYPE" == "ext4" ]]; then
free_mib=$(ext4_free_mib "$NAME")
else
free_mib=$(btrfs_free_mib "$NAME")
fi
free_mib=${free_mib:-0}
# Shrinkable: not /, enough free space
local ok=0
[[ "$MOUNTPOINT" != "/" && "$free_mib" -ge "$MIN_FREE_MIB" ]] && ok=1
PT_DEV[$PT_IDX]="$NAME"
PT_DISK[$PT_IDX]="$disk"
PT_PNUM[$PT_IDX]="$pnum"
PT_SIZE[$PT_IDX]="$SIZE"
PT_FS[$PT_IDX]="$FSTYPE"
PT_LABEL[$PT_IDX]="${LABEL:--}"
PT_UUID[$PT_IDX]="${UUID:--}"
PT_MNT[$PT_IDX]="${MOUNTPOINT:--}"
PT_FREE[$PT_IDX]="$free_mib"
PT_OK[$PT_IDX]="$ok"
(( PT_IDX++ )) || true
done < <(lsblk -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINT,TYPE,PKNAME -p -P -n 2>/dev/null)
}
collect_free_space() {
local disk
while IFS= read -r disk; do
[[ -b "$disk" ]] || continue
while read -r s e sz; do
FS_DISK[$FS_IDX]="$disk"
FS_START[$FS_IDX]="$s"
FS_END[$FS_IDX]="$e"
FS_SIZE[$FS_IDX]="$sz"
(( FS_IDX++ )) || true
done < <(
parted -s "$disk" -m unit MiB print free 2>/dev/null \
| awk -F'[;:]' -v min="$SHRINK_MIB" '
$1+0 > 0 {
for (i = 1; i <= NF; i++) {
if ($i == "free") {
start=$2; end=$3; size=$4;
gsub(/MiB/,"",start); gsub(/MiB/,"",end); gsub(/MiB/,"",size);
s=int(start+0); e=int(end+0); sz=int(size+0);
if (sz >= min) print s " " e " " sz;
break
}
}
}'
)
done < <(lsblk -d -p -n -o NAME 2>/dev/null)
}
# ── Table display ─────────────────────────────────────────────────────────────
show_table() {
hr
printf " %-4s %-14s %-6s %-8s %-10s %-6s %-18s %-36s %s\n" \
"#" "Device" "Disk" "Size" "Free (MiB)" "FS" "Label" "UUID" "Mountpoint"
hr
local i mark note
for (( i=0; i<PT_IDX; i++ )); do
mark=" "
note=""
if [[ "${PT_OK[$i]}" == "1" ]]; then
mark="* "
elif [[ "${PT_MNT[$i]}" == "/" ]]; then
note=" (root — skip)"
else
note=" (not enough free space)"
fi
printf "%s%-4s %-14s %-6s %-8s %-10s %-6s %-18s %-36s %s%s\n" \
"$mark" \
"$((i+1))" \
"${PT_DEV[$i]}" \
"${PT_DISK[$i]##/dev/}" \
"${PT_SIZE[$i]}" \
"${PT_FREE[$i]}" \
"${PT_FS[$i]}" \
"${PT_LABEL[$i]}" \
"${PT_UUID[$i]}" \
"${PT_MNT[$i]}" \
"$note"
done
hr
printf " * = shrinkable: will be reduced by %s MiB to create OEMDRV\n" "$SHRINK_MIB"
echo
}
show_free_table() {
[[ $FS_IDX -gt 0 ]] || return
echo
echo "Unpartitioned free space regions (>= ${SHRINK_MIB} MiB):"
hr
printf " %-4s %-14s %-12s %-12s %s\n" "#" "Disk" "Start (MiB)" "End (MiB)" "Size (MiB)"
hr
for (( i=0; i<FS_IDX; i++ )); do
printf " %-4s %-14s %-12s %-12s %s\n" \
"$((i+1))" \
"${FS_DISK[$i]}" \
"${FS_START[$i]}" \
"${FS_END[$i]}" \
"${FS_SIZE[$i]}"
done
hr
echo
}
# ── Partition geometry via parted -m ─────────────────────────────────────────
part_end_mib() {
parted -s "$1" -m unit MiB print 2>/dev/null \
| awk -F: -v p="$2" '$1==p{gsub("MiB","",$3); printf "%.0f\n",$3; exit}'
}
part_start_mib() {
parted -s "$1" -m unit MiB print 2>/dev/null \
| awk -F: -v p="$2" '$1==p{gsub("MiB","",$2); printf "%.0f\n",$2; exit}'
}
# Device name for a new partition on a given disk
new_part_device() {
local disk="$1" pnum="$2"
if [[ "$disk" =~ (nvme|mmcblk) ]]; then
echo "${disk}p${pnum}"
else
echo "${disk}${pnum}"
fi
}
# ── Main ──────────────────────────────────────────────────────────────────────
require_root
check_tools
info "Scanning for shrinkable partitions and unpartitioned free space..."
collect_partitions
collect_free_space
[[ $PT_IDX -gt 0 || $FS_IDX -gt 0 ]] \
|| die "No ext4 or btrfs partitions and no free disk space found on this system."
# Count shrinkable partitions
shrink_count=0
for (( i=0; i<PT_IDX; i++ )); do [[ "${PT_OK[$i]}" == "1" ]] && (( shrink_count++ )) || true; done
[[ $PT_IDX -gt 0 ]] && show_table
show_free_table
[[ $shrink_count -gt 0 || $FS_IDX -gt 0 ]] \
|| die "No shrinkable partitions and no free space found. A non-root ext4 or btrfs partition needs at least ${MIN_FREE_MIB} MiB free."
# ── Selection ─────────────────────────────────────────────────────────────────
MODE="" # "freespace" | "shrink"
SEL=-1
while true; do
echo
if [[ $FS_IDX -gt 0 && $shrink_count -gt 0 ]]; then
read -r -p "Enter f<n> to use free space, s<n> to shrink a partition, or q to quit: " INPUT
elif [[ $FS_IDX -gt 0 ]]; then
read -r -p "Enter number of free space region to use, or q to quit: " INPUT
[[ "$INPUT" =~ ^[0-9]+$ ]] && INPUT="f${INPUT}"
else
read -r -p "Enter number of partition to shrink, or q to quit: " INPUT
[[ "$INPUT" =~ ^[0-9]+$ ]] && INPUT="s${INPUT}"
fi
[[ "$INPUT" =~ ^[qQ]$ ]] && { echo "Aborted."; exit 0; }
if [[ "$INPUT" =~ ^[fF]([0-9]+)$ ]]; then
local_i=$(( ${BASH_REMATCH[1]} - 1 ))
(( local_i >= 0 && local_i < FS_IDX )) || { echo " Number out of range."; continue; }
MODE="freespace"; SEL=$local_i; break
elif [[ "$INPUT" =~ ^[sS]([0-9]+)$ ]]; then
local_i=$(( ${BASH_REMATCH[1]} - 1 ))
(( local_i >= 0 && local_i < PT_IDX )) || { echo " Number out of range."; continue; }
[[ "${PT_OK[$local_i]}" == "1" ]] || { echo " That partition cannot be shrunk (see marks above)."; continue; }
MODE="shrink"; SEL=$local_i; break
else
echo " Invalid input."
fi
done
# ── Confirm ───────────────────────────────────────────────────────────────────
echo
if [[ "$MODE" == "freespace" ]]; then
WORK_DISK="${FS_DISK[$SEL]}"
OEMDRV_START="${FS_START[$SEL]}"
OEMDRV_END=$(( FS_START[$SEL] + SHRINK_MIB ))
echo " Disk : $WORK_DISK"
echo " Free region : ${FS_START[$SEL]} MiB ${FS_END[$SEL]} MiB (${FS_SIZE[$SEL]} MiB available)"
echo " New OEMDRV : ${OEMDRV_START} MiB ${OEMDRV_END} MiB (${SHRINK_MIB} MiB)"
echo " It will be mounted at $MOUNT_POINT and the repository cloned into it."
else
S_DEV="${PT_DEV[$SEL]}"
S_DISK="${PT_DISK[$SEL]}"
S_PNUM="${PT_PNUM[$SEL]}"
S_FS="${PT_FS[$SEL]}"
S_MNT="${PT_MNT[$SEL]}"
WORK_DISK="$S_DISK"
echo " Partition : $S_DEV (${PT_FS[$SEL]}, ${PT_SIZE[$SEL]}, ${PT_FREE[$SEL]} MiB free)"
echo " Disk : $S_DISK Partition number: $S_PNUM"
echo " Mountpoint: $S_MNT"
echo
echo " $S_DEV will be shrunk by ${SHRINK_MIB} MiB."
echo " A new ${SHRINK_MIB} MiB BTRFS partition labeled OEMDRV will be created."
echo " It will be mounted at $MOUNT_POINT and the repository cloned into it."
fi
echo
read -r -p " Type YES (uppercase) to confirm, anything else to abort: " CONFIRM
[[ "$CONFIRM" == "YES" ]] || { echo "Aborted."; exit 0; }
# ── Shrink path: geometry + filesystem resize + partition table update ─────────
WAS_MOUNTED=0
ORIG_MNT=""
if [[ "$MODE" == "shrink" ]]; then
info "Reading partition geometry on $S_DISK..."
CURR_END=$(part_end_mib "$S_DISK" "$S_PNUM")
CURR_START=$(part_start_mib "$S_DISK" "$S_PNUM")
[[ -n "$CURR_END" && -n "$CURR_START" ]] \
|| die "Could not read geometry for partition $S_PNUM on $S_DISK."
NEW_END=$(( CURR_END - SHRINK_MIB ))
NEW_FS_MIB=$(( NEW_END - CURR_START ))
OEMDRV_START=$NEW_END
OEMDRV_END=$CURR_END
echo " Current partition : ${CURR_START} MiB ${CURR_END} MiB"
echo " After shrink : ${CURR_START} MiB ${NEW_END} MiB (filesystem: ${NEW_FS_MIB} MiB)"
echo " New OEMDRV : ${OEMDRV_START} MiB ${OEMDRV_END} MiB (${SHRINK_MIB} MiB)"
if [[ "$S_FS" == "ext4" ]]; then
# ext4 requires offline resize — unmount first
if [[ "$S_MNT" != "-" && -n "$S_MNT" ]]; then
info "Unmounting $S_DEV from $S_MNT..."
ORIG_MNT="$S_MNT"
umount "$S_DEV" || die "Cannot unmount $S_DEV. Close all open files and try again."
WAS_MOUNTED=1
fi
info "Running e2fsck on $S_DEV..."
e2fsck -f "$S_DEV" || die "e2fsck found errors on $S_DEV. Fix them before retrying."
info "Shrinking ext4 filesystem to ${NEW_FS_MIB} MiB..."
resize2fs "$S_DEV" "${NEW_FS_MIB}M" || die "resize2fs failed."
elif [[ "$S_FS" == "btrfs" ]]; then
# btrfs supports online resize — use existing mount point if available,
# otherwise mount temporarily
info "Shrinking btrfs filesystem on $S_DEV to ${NEW_FS_MIB} MiB..."
if [[ "$S_MNT" != "-" && -n "$S_MNT" ]]; then
btrfs filesystem resize "${NEW_FS_MIB}m" "$S_MNT" \
|| die "btrfs filesystem resize failed."
else
TMP_MNT=$(mktemp -d /tmp/oemdrv_btrfs.XXXXXX)
mount "$S_DEV" "$TMP_MNT" || { rmdir "$TMP_MNT"; die "Cannot mount $S_DEV for btrfs resize."; }
btrfs filesystem resize "${NEW_FS_MIB}m" "$TMP_MNT" \
|| { umount "$TMP_MNT"; rmdir "$TMP_MNT"; die "btrfs filesystem resize failed."; }
sync
umount "$TMP_MNT" && rmdir "$TMP_MNT"
fi
fi
info "Shrinking partition $S_PNUM to ${NEW_END} MiB in partition table..."
SECTOR_SIZE=$(cat /sys/block/"${S_DISK##*/}"/queue/hw_sector_size 2>/dev/null || echo 512)
NEW_END_SEC=$(( NEW_END * 1048576 / SECTOR_SIZE ))
sfdisk -d "$S_DISK" | awk -v dev="$S_DEV" -v new_end="$NEW_END_SEC" '
$0 ~ "^" dev " : " {
match($0, /start= *([0-9]+)/, a)
sub(/size= *[0-9]+/, "size= " (new_end - a[1]+0))
}
{ print }
' | sfdisk --no-reread --force "$S_DISK" \
|| die "sfdisk partition resize failed."
fi
# ── Create OEMDRV partition ───────────────────────────────────────────────────
info "Creating new OEMDRV partition (${OEMDRV_START}${OEMDRV_END} MiB) on $WORK_DISK..."
printf 'Yes\n' | parted "$WORK_DISK" mkpart anacondainstall btrfs "${OEMDRV_START}MiB" "${OEMDRV_END}MiB" \
|| die "parted mkpart failed. Check that the target area is free space on $WORK_DISK."
partprobe "$WORK_DISK"
sleep 1
# Determine new partition number (highest on the disk after partprobe)
NEW_PNUM=$(parted -s "$WORK_DISK" -m unit MiB print 2>/dev/null \
| awk -F: '/^[0-9]/{n=$1} END{print n}')
[[ -n "$NEW_PNUM" ]] || die "Could not determine new partition number on $WORK_DISK."
OEMDRV_DEV=$(new_part_device "$WORK_DISK" "$NEW_PNUM")
[[ -b "$OEMDRV_DEV" ]] || die "New partition device $OEMDRV_DEV not found after partprobe."
info "New partition device: $OEMDRV_DEV"
# ── Format ────────────────────────────────────────────────────────────────────
info "Formatting $OEMDRV_DEV as BTRFS (label: $OEMDRV_LABEL)..."
mkfs.btrfs -f -L "$OEMDRV_LABEL" "$OEMDRV_DEV" || die "mkfs.btrfs failed."
# ── Remount original partition (shrink path only) ─────────────────────────────
if [[ $WAS_MOUNTED -eq 1 && -n "$ORIG_MNT" ]]; then
info "Remounting $S_DEV to $ORIG_MNT..."
mount "$S_DEV" "$ORIG_MNT" \
|| echo "WARNING: Could not remount $S_DEV to $ORIG_MNT — remount manually."
fi
# ── Mount OEMDRV ──────────────────────────────────────────────────────────────
info "Mounting $OEMDRV_DEV to $MOUNT_POINT (options: $MOUNT_OPTS)..."
[[ -d "$MOUNT_POINT" ]] || mkdir -p "$MOUNT_POINT"
mount -o "$MOUNT_OPTS" "$OEMDRV_DEV" "$MOUNT_POINT" || die "mount failed."
# ── Clone repository ──────────────────────────────────────────────────────────
info "Cloning $REPO_URL into $MOUNT_POINT..."
cd "$MOUNT_POINT" || die "Cannot cd to $MOUNT_POINT."
git clone --progress --depth 1 "$REPO_URL" . || die "git clone failed."
# ── Done ──────────────────────────────────────────────────────────────────────
info "Done."
echo
echo " OEMDRV device : $OEMDRV_DEV"
echo " Mounted at : $MOUNT_POINT"
echo
echo "Next steps:"
echo " 1. cp $MOUNT_POINT/config/setup_system.conf.dist \\"
echo " $MOUNT_POINT/config/setup_system.conf"
echo " 2. Edit setup_system.conf with your domain, IPA/Nextcloud FQDNs, and paths."
echo " 3. Boot the Kickstart installer — it will detect the OEMDRV partition automatically."
echo