diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-22 18:03:40 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-22 18:03:40 -0500 |
| commit | b6525a50fabf3aedf41eee70c164519b00d27704 (patch) | |
| tree | 0b9900eb584509051c83ebed0ed1427ee9bee9e7 /tests/unit/test_archangel.bats | |
| parent | 4ef30e5c84ab22ba1724608009093d6725a1ceda (diff) | |
| download | archangel-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 'tests/unit/test_archangel.bats')
| -rw-r--r-- | tests/unit/test_archangel.bats | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/tests/unit/test_archangel.bats b/tests/unit/test_archangel.bats index c7bbc56..c38dcb8 100644 --- a/tests/unit/test_archangel.bats +++ b/tests/unit/test_archangel.bats @@ -239,3 +239,136 @@ setup() { [[ " ${CALLS[*]} " == *" zpool list zroot "* ]] [[ " ${CALLS[*]} " != *" zpool export"* ]] } + +@test "install_failure_cleanup ZFS path falls back to lazy unmount when a mount is busy" { + FILESYSTEM=zfs + POOL_NAME=zroot + CALLS=() + + # A pacstrap-interrupted target can leave busy mounts that a plain + # umount can't release; cleanup must retry lazily so the retry sees a + # clean disk. Non-lazy umount fails here; the -l fallback succeeds. + umount() { + CALLS+=("umount $*") + [[ "$*" == *"-l"* ]] && return 0 + return 1 + } + zpool() { CALLS+=("zpool $*"); return 0; } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + [[ " ${CALLS[*]} " == *" umount -l /mnt/efi "* ]] + [[ " ${CALLS[*]} " == *" umount -R -l /mnt "* ]] + # The pool still gets exported after the lazy unmount. + [[ " ${CALLS[*]} " == *" zpool export zroot "* ]] +} + +@test "install_failure_cleanup Btrfs path falls back to lazy unmount when EFI is busy" { + FILESYSTEM=btrfs + CALLS=() + + umount() { + CALLS+=("umount $*") + [[ "$*" == *"-l"* ]] && return 0 + return 1 + } + btrfs_cleanup() { CALLS+=("btrfs_cleanup"); } + btrfs_close_encryption() { CALLS+=("btrfs_close_encryption"); } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + [[ " ${CALLS[*]} " == *" umount -l /mnt/efi "* ]] +} + +############################# +# validate_environment +############################# +# Boundary wrappers (is_uefi_boot, required_commands) are stubbed so the +# composition's fail-fast wiring is exercised without depending on the +# host's firmware mode or installed tools. The real command list lives in +# test_common.bats; the real UEFI/network probes run in the VM harness. + +@test "validate_environment errors when not booted in UEFI mode" { + is_uefi_boot() { return 1; } + required_commands() { return 0; } + FILESYSTEM=zfs + run validate_environment + [ "$status" -eq 1 ] + [[ "$output" == *"UEFI"* ]] +} + +@test "validate_environment errors when a required command is missing" { + is_uefi_boot() { return 0; } + required_commands() { echo "definitely-not-a-real-cmd-xyz"; } + FILESYSTEM=zfs + run validate_environment + [ "$status" -eq 1 ] + [[ "$output" == *"definitely-not-a-real-cmd-xyz"* ]] +} + +@test "validate_environment passes when UEFI present and commands resolve" { + is_uefi_boot() { return 0; } + required_commands() { echo "bash"; } + FILESYSTEM=zfs + run validate_environment + [ "$status" -eq 0 ] +} + +############################# +# validate_install_targets +############################# +# disk_in_use / disk_size_bytes / network_available are the system-boundary +# wrappers; stubbing them drives the real composition + real +# disk_meets_min_size. Live probes run in the VM harness on the happy path. + +@test "validate_install_targets errors when a disk is in use" { + SELECTED_DISKS=(/dev/sda) + disk_in_use() { return 0; } + disk_size_bytes() { echo 500107862016; } + network_available() { return 0; } + run validate_install_targets + [ "$status" -eq 1 ] + [[ "$output" == *"in use"* ]] +} + +@test "validate_install_targets errors when a disk is too small" { + SELECTED_DISKS=(/dev/sda) + disk_in_use() { return 1; } + disk_size_bytes() { echo 1000000; } + network_available() { return 0; } + run validate_install_targets + [ "$status" -eq 1 ] + [[ "$output" == *"too small"* ]] +} + +@test "validate_install_targets errors when disk size is unreadable" { + SELECTED_DISKS=(/dev/sda) + disk_in_use() { return 1; } + disk_size_bytes() { echo ""; } + network_available() { return 0; } + run validate_install_targets + [ "$status" -eq 1 ] +} + +@test "validate_install_targets errors when the network is unreachable" { + SELECTED_DISKS=(/dev/sda) + disk_in_use() { return 1; } + disk_size_bytes() { echo 500107862016; } + network_available() { return 1; } + run validate_install_targets + [ "$status" -eq 1 ] + [[ "$output" == *"network"* || "$output" == *"connectivity"* ]] +} + +@test "validate_install_targets passes when disks idle, large enough, network up" { + SELECTED_DISKS=(/dev/sda /dev/sdb) + disk_in_use() { return 1; } + disk_size_bytes() { echo 500107862016; } + network_available() { return 0; } + run validate_install_targets + [ "$status" -eq 0 ] +} |
