aboutsummaryrefslogtreecommitdiff
path: root/installer/lib
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-22 18:03:40 -0500
committerCraig Jennings <c@cjennings.net>2026-05-22 18:03:40 -0500
commitb6525a50fabf3aedf41eee70c164519b00d27704 (patch)
tree0b9900eb584509051c83ebed0ed1427ee9bee9e7 /installer/lib
parent4ef30e5c84ab22ba1724608009093d6725a1ceda (diff)
downloadarchangel-b6525a50fabf3aedf41eee70c164519b00d27704.tar.gz
archangel-b6525a50fabf3aedf41eee70c164519b00d27704.zip
feat(install): add pre-flight environment and disk-target validation
archangel went straight from filesystem selection into a destructive install behind only a root check and a ZFS module load. A missing tool, a BIOS boot, a too-small or in-use disk, or a dead network surfaced as a confusing abort partway through, sometimes after partitioning had already run. Two gates now fail fast. validate_environment runs after filesystem selection, before any disk is touched: it confirms UEFI boot mode and that every required command is present, with the list coming from a new required_commands helper built like pacstrap_packages. validate_install_targets runs after disk selection, before the first wipe: it refuses a target that's mounted, holds active swap, or belongs to an imported pool or md array, rejects disks under 20 GB, and confirms a mirror is reachable via DNS plus a TCP probe (no ICMP, since some networks drop it). I folded the install_failure_cleanup hardening into the same change. It now falls back to lazy unmounts, so a pacstrap-interrupted target with busy bind mounts still releases the pool and unmounts the EFI partition. Without that, the disk-in-use guard would block the very retry the cleanup exists to enable. "Re-run to retry" only holds if the disk is genuinely freed first. The 20 GB floor is decimal on purpose. It reads as the natural minimum and clears a 20 GiB disk image with headroom instead of sitting on the boundary.
Diffstat (limited to 'installer/lib')
-rw-r--r--installer/lib/common.sh21
-rw-r--r--installer/lib/disk.sh59
2 files changed, 80 insertions, 0 deletions
diff --git a/installer/lib/common.sh b/installer/lib/common.sh
index 2cd4798..7998eeb 100644
--- a/installer/lib/common.sh
+++ b/installer/lib/common.sh
@@ -102,6 +102,27 @@ pacstrap_packages() {
printf '%s\n' "${common[@]}" "${fs_specific[@]}"
}
+# Print the external commands the installer needs for the given filesystem,
+# one per line: common partitioning/bootstrap tools first, then
+# filesystem-specific ones. validate_environment loops over these and
+# require_command's each, so a missing tool fails fast on the live ISO
+# instead of mid-install. Returns 1 for unknown filesystem.
+#
+# Usage: mapfile -t cmds < <(required_commands zfs)
+required_commands() {
+ local fs="$1"
+ local common=(
+ sgdisk wipefs partprobe mkfs.fat pacstrap
+ )
+ local fs_specific
+ case "$fs" in
+ zfs) fs_specific=(zpool zfs) ;;
+ btrfs) fs_specific=(mkfs.btrfs grub-install) ;;
+ *) return 1 ;;
+ esac
+ printf '%s\n' "${common[@]}" "${fs_specific[@]}"
+}
+
#############################
# Password / Passphrase Input
#############################
diff --git a/installer/lib/disk.sh b/installer/lib/disk.sh
index b548b4f..ae7801b 100644
--- a/installer/lib/disk.sh
+++ b/installer/lib/disk.sh
@@ -131,3 +131,62 @@ select_disks() {
info "Selected disks: ${SELECTED_DISKS[*]}"
}
+#############################
+# Pre-flight: Disk Safety
+#############################
+
+# Minimum usable install disk. Root plus the 50G reservation, packages, and
+# snapshots needs real headroom; below this the install fails partway
+# through. 20 GB is a hard floor (validate_install_targets errors out).
+# Decimal GB (disk-vendor sizing) on purpose: it reads as the natural "20GB"
+# minimum and clears a 20 GiB disk image with headroom rather than sitting
+# exactly on the boundary.
+MIN_DISK_BYTES=20000000000 # 20 * 10^9 (20 GB)
+
+# Pure size predicate: succeed only when <bytes> is a non-negative integer
+# meeting MIN_DISK_BYTES. Non-numeric or empty input fails (treated as an
+# unknown size, which is itself a reason not to proceed).
+disk_meets_min_size() {
+ local bytes="$1"
+ [[ "$bytes" =~ ^[0-9]+$ ]] || return 1
+ (( bytes >= MIN_DISK_BYTES ))
+}
+
+# Size of a block device in bytes (live query). Thin wrapper over blockdev;
+# exercised by the VM integration harness rather than unit tests.
+disk_size_bytes() {
+ blockdev --getsize64 "$1" 2>/dev/null
+}
+
+# Succeed (return 0) when <disk> is in active use and must NOT be wiped:
+# any partition mounted, active swap on it, or membership in an imported
+# zpool or assembled md array. Over-detection errs on the safe side
+# (refuse). Live-state predicate — validated in the VM harness, where the
+# install disks are deliberately idle so the happy path returns 1.
+disk_in_use() {
+ local disk="$1"
+ local base
+ base=$(basename "$disk")
+
+ # Any mountpoint on the disk or its children.
+ if lsblk -nro MOUNTPOINT "$disk" 2>/dev/null | grep -q .; then
+ return 0
+ fi
+ # Active swap on the disk or a partition of it.
+ if swapon --show=NAME --noheadings 2>/dev/null | grep -q "^${disk}"; then
+ return 0
+ fi
+ # Member of an imported zpool. -P prints full device paths (/dev/vda2),
+ # so a fixed-string match on the disk path catches partition members too
+ # — a plain word match on the bare name would miss "vda2".
+ if command_exists zpool && zpool status -LP 2>/dev/null | grep -qF "$disk"; then
+ return 0
+ fi
+ # Member of an assembled md array. /proc/mdstat lists bare partition names
+ # (vda1[0]); substring-match the disk name (over-match errs toward refuse).
+ if grep -qsF "$base" /proc/mdstat 2>/dev/null; then
+ return 0
+ fi
+ return 1
+}
+