diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-23 20:55:07 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-23 20:55:07 -0400 |
| commit | edb5016809f3bc657283d8c2402970dbbab3c5cf (patch) | |
| tree | f31ed6432b921f117a2abca1b9c12efe8f8d7b26 | |
| parent | f0f56e1fe2e2bbb3a57bb61235e67c0bdc8402ae (diff) | |
| download | archangel-edb5016809f3bc657283d8c2402970dbbab3c5cf.tar.gz archangel-edb5016809f3bc657283d8c2402970dbbab3c5cf.zip | |
fix(installer): RAID validation, set -e fix, drop dead shadow branch
Two installer cleanups from the todo backlog.
validate_config now rejects a RAID_LEVEL the selected disk count can't support, guarding the unattended path (the interactive path already constrains the choice). While adding it I found a latent bug: the error loop's ((errors++)) returned 0 on the first error and tripped set -e in the monolith's `[[ UNATTENDED == true ]] && validate_config` call, aborting after one warning instead of listing every problem. Switched to pre-increment so the count accumulates as designed. Added four bats cases, including one that runs validate_config under set -e outside bats' run shield.
build.sh dropped the dead shadow-file rebuild else-branch. The profile is always copied fresh from releng (which ships /etc/shadow), so the branch never ran, and its hardcoded account list had drifted from what releng provides. Replaced with an assertion that fails the build loudly if the file is ever missing.
| -rwxr-xr-x | build.sh | 35 | ||||
| -rw-r--r-- | installer/lib/config.sh | 22 | ||||
| -rw-r--r-- | tests/unit/test_config.bats | 58 |
3 files changed, 82 insertions, 33 deletions
@@ -447,33 +447,14 @@ EOF info "Setting root password for live ISO..." # Generate password hash PASS_HASH=$(openssl passwd -6 "$LIVE_ROOT_PASSWORD") -# Modify the existing shadow file's root entry (don't replace entire file) -# The releng template has multiple accounts; replacing breaks the file -if [[ -f "$PROFILE_DIR/airootfs/etc/shadow" ]]; then - sed -i "s|^root:[^:]*:|root:${PASS_HASH}:|" "$PROFILE_DIR/airootfs/etc/shadow" -else - # Fallback: create complete shadow file if it doesn't exist - cat > "$PROFILE_DIR/airootfs/etc/shadow" << EOF -root:${PASS_HASH}:19000:0:99999:7::: -bin:!*:19000:::::: -daemon:!*:19000:::::: -mail:!*:19000:::::: -ftp:!*:19000:::::: -http:!*:19000:::::: -nobody:!*:19000:::::: -dbus:!*:19000:::::: -systemd-coredump:!*:19000:::::: -systemd-network:!*:19000:::::: -systemd-oom:!*:19000:::::: -systemd-journal-remote:!*:19000:::::: -systemd-resolve:!*:19000:::::: -systemd-timesync:!*:19000:::::: -tss:!*:19000:::::: -uuidd:!*:19000:::::: -polkitd:!*:19000:::::: -avahi:!*:19000:::::: -EOF -fi +# Modify the existing shadow file's root entry (don't replace the whole +# file — the releng template ships /etc/shadow with multiple accounts and +# rewriting it from scratch would drop them). The profile is always copied +# fresh from releng above, so the file is present; if it's missing, that +# copy is broken — fail loudly rather than silently rebuilding a stale list. +[[ -f "$PROFILE_DIR/airootfs/etc/shadow" ]] \ + || error "Expected shadow file missing: $PROFILE_DIR/airootfs/etc/shadow (releng profile copy broken?)" +sed -i "s|^root:[^:]*:|root:${PASS_HASH}:|" "$PROFILE_DIR/airootfs/etc/shadow" chmod 400 "$PROFILE_DIR/airootfs/etc/shadow" # Allow root SSH login with password (for testing) diff --git a/installer/lib/config.sh b/installer/lib/config.sh index 3ba2bb3..ed54e36 100644 --- a/installer/lib/config.sh +++ b/installer/lib/config.sh @@ -116,20 +116,30 @@ check_config() { validate_config() { local errors=0 - [[ -z "$HOSTNAME" ]] && { warn "HOSTNAME not set"; ((errors++)); } - [[ -z "$TIMEZONE" ]] && { warn "TIMEZONE not set"; ((errors++)); } - [[ ${#SELECTED_DISKS[@]} -eq 0 ]] && { warn "No disks selected"; ((errors++)); } - [[ -z "$ROOT_PASSWORD" ]] && { warn "ROOT_PASSWORD not set"; ((errors++)); } + [[ -z "$HOSTNAME" ]] && { warn "HOSTNAME not set"; ((++errors)); } + [[ -z "$TIMEZONE" ]] && { warn "TIMEZONE not set"; ((++errors)); } + [[ ${#SELECTED_DISKS[@]} -eq 0 ]] && { warn "No disks selected"; ((++errors)); } + [[ -z "$ROOT_PASSWORD" ]] && { warn "ROOT_PASSWORD not set"; ((++errors)); } # Validate disks exist for disk in "${SELECTED_DISKS[@]}"; do - [[ -b "$disk" ]] || { warn "Disk not found: $disk"; ((errors++)); } + [[ -b "$disk" ]] || { warn "Disk not found: $disk"; ((++errors)); } done # Validate timezone if [[ -n "$TIMEZONE" && ! -f "/usr/share/zoneinfo/$TIMEZONE" ]]; then warn "Invalid timezone: $TIMEZONE" - ((errors++)) + ((++errors)) + fi + + # Validate the RAID level against the selected disk count. The + # interactive path only offers levels valid for the count, so this + # guards the unattended config, where RAID_LEVEL is set by hand and + # can name a level the disk count can't support. raid_is_valid treats + # an empty level on a single disk (no RAID) as valid. + if ! raid_is_valid "$RAID_LEVEL" "${#SELECTED_DISKS[@]}"; then + warn "Invalid RAID_LEVEL '$RAID_LEVEL' for ${#SELECTED_DISKS[@]} disk(s)" + ((++errors)) fi if [[ $errors -gt 0 ]]; then diff --git a/tests/unit/test_config.bats b/tests/unit/test_config.bats index af23e4a..4169c5e 100644 --- a/tests/unit/test_config.bats +++ b/tests/unit/test_config.bats @@ -5,6 +5,8 @@ setup() { # shellcheck disable=SC1091 source "${BATS_TEST_DIRNAME}/../../installer/lib/common.sh" # shellcheck disable=SC1091 + source "${BATS_TEST_DIRNAME}/../../installer/lib/raid.sh" + # shellcheck disable=SC1091 source "${BATS_TEST_DIRNAME}/../../installer/lib/config.sh" } @@ -93,6 +95,62 @@ EOF [[ "$output" == *"4 error"* ]] } +@test "validate_config under set -e reports every error, not just the first" { + # Reproduces the monolith's call structure: `set -e` is active and + # validate_config is invoked as the final command of an && list. A + # post-increment that returns the pre-increment value (0 on the first + # error) trips set -e and aborts the function after one warning. This + # test runs outside bats' `run` shield (which sets +e) so the real + # accumulate-and-report behavior is exercised. + run bash -c ' + set -e + source "'"${BATS_TEST_DIRNAME}"'/../../installer/lib/common.sh" + source "'"${BATS_TEST_DIRNAME}"'/../../installer/lib/raid.sh" + source "'"${BATS_TEST_DIRNAME}"'/../../installer/lib/config.sh" + HOSTNAME=""; TIMEZONE=""; SELECTED_DISKS=(); ROOT_PASSWORD="" + UNATTENDED=true + [[ "$UNATTENDED" == true ]] && validate_config + ' + [ "$status" -eq 1 ] + [[ "$output" == *"HOSTNAME not set"* ]] + [[ "$output" == *"TIMEZONE not set"* ]] + [[ "$output" == *"No disks selected"* ]] + [[ "$output" == *"ROOT_PASSWORD not set"* ]] + [[ "$output" == *"4 error"* ]] +} + +@test "validate_config rejects a RAID_LEVEL invalid for the disk count" { + HOSTNAME=h + TIMEZONE=UTC + ROOT_PASSWORD=x + SELECTED_DISKS=(/dev/sda /dev/sdb) + RAID_LEVEL=raidz1 + run validate_config + [ "$status" -eq 1 ] + [[ "$output" == *"Invalid RAID_LEVEL"* ]] + [[ "$output" == *"raidz1"* ]] +} + +@test "validate_config accepts a RAID_LEVEL valid for the disk count" { + HOSTNAME=h + TIMEZONE=UTC + ROOT_PASSWORD=x + SELECTED_DISKS=(/dev/sda /dev/sdb /dev/sdc) + RAID_LEVEL=raidz1 + run validate_config + [[ "$output" != *"Invalid RAID_LEVEL"* ]] +} + +@test "validate_config accepts an empty RAID_LEVEL for a single disk" { + HOSTNAME=h + TIMEZONE=UTC + ROOT_PASSWORD=x + SELECTED_DISKS=(/dev/sda) + RAID_LEVEL="" + run validate_config + [[ "$output" != *"Invalid RAID_LEVEL"* ]] +} + @test "validate_config rejects an invalid timezone" { HOSTNAME="h" TIMEZONE="Not/A_Real_Zone_xyz" |
