diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c8e2d80 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/client_software/0050_nextcloud_desktopclient/user_run.sh b/client_software/0050_nextcloud_desktopclient/user_run.sh index d822e8f..8657bf7 100755 --- a/client_software/0050_nextcloud_desktopclient/user_run.sh +++ b/client_software/0050_nextcloud_desktopclient/user_run.sh @@ -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 diff --git a/install.md b/install.md new file mode 100644 index 0000000..caec2b1 --- /dev/null +++ b/install.md @@ -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) | diff --git a/system_setup/install.sh b/system_setup/install.sh new file mode 100755 index 0000000..6496b26 --- /dev/null +++ b/system_setup/install.sh @@ -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= ${SHRINK_MIB} MiB):" + hr + printf " %-4s %-14s %-12s %-12s %s\n" "#" "Disk" "Start (MiB)" "End (MiB)" "Size (MiB)" + hr + for (( i=0; i/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 to use free space, s 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