#!/bin/bash # bootstrap-modes.sh — R-21 slice C regression harness for felhom-bootstrap.sh's two modes. Runs as # root inside a throwaway container (felhom-iso-assistant:trixie — python3 needed by the pairing # parsers; writes /etc/felhom etc.); fakes curl + host-install + systemctl + sleep on PATH. Asserts: # D (regression): a DIRECT env (customer-id + passphrase) enters run_direct and makes ZERO calls to # /api/v1/appliance/* — the pairing code path is provably not entered. # P (pairing+delivery, v1.21.0 loop semantics): an env WITHOUT customer-id/passphrase enters # run_pairing, POSTs /appliance/register, persists the token, polls 204 INSIDE one # invocation (fake sleep counts the waits; the script never exits between polls), # then consumes the 200 delivery, writes the direct env and runs host-install. # 410: a consumed-delivery poll exits non-zero (the crash-window hand-back to systemd). # G1 (in D+P): hub reachable -> the network gate returns with ZERO new behavior — no ip/ # ifreload/dhclient fake is ever touched (B'-style invariant). # G2 sweep-ok: hub unreachable + no state.json -> the sweep re-points vmbr0 to the NIC that # reaches the hub, persists atomically (winner + inet dhcp; original saved as # interfaces.felhom-bak), logs "network self-heal", and PROCEEDS to pairing. # G3 sweep-fail: no NIC reaches the hub -> console screen painted AND the interfaces file is # BYTE-IDENTICAL (a failed sweep must never leave a half-rewritten config). # G4 state-file: state.json present -> screen + retry only; the sweep is NEVER invoked # (ifreload/dhclient call-count zero), interfaces untouched. # (Updated for v1.21.0/R-33: the old one-poll-per-invocation scenarios expected a non-zero exit on # 204, which the in-script wait deliberately no longer does — waiting is not failing.) set -uo pipefail BSTRAP=/work/felhom-bootstrap.sh FAKE=/work/fakebin; rm -rf "$FAKE"; mkdir -p "$FAKE" CALLS=/work/curl.log export PATH="$FAKE:$PATH" fail=0 # --- console capture (R-586) ------------------------------------------------------------------- # # THE SCRIPT WRITES EACH BANNER WITH `>`, WHICH TRUNCATES A FILE. On a real console that is a # character device and truncation is a no-op — every paint simply appears. Pointed at a plain FILE, # as this harness did until now, each banner ERASES the one before it. # # That is not hypothetical: ISO 1.28.0 (commit c033b3b, R-535) added print_bound_banner, which paints # right after the pairing banner in the same invocation. From that commit onward # `grep 'Párosító kód' /work/console.out` looked for a banner the NEXT paint had already wiped, and # the two R-496 checks below have been FAILING ever since. Nobody saw it: this harness is not in CI # and is not in repo_gates.py, so it runs only when someone runs it. # # A FIFO restores the device semantics: opening it with `>` truncates nothing, and a background # reader accumulates every paint. `console_reset` starts a fresh capture per scenario. CONSOLE_FIFO=/work/console.fifo console_reset() { [ -n "${CONSOLE_CAT_PID:-}" ] && kill "$CONSOLE_CAT_PID" 2>/dev/null rm -f /work/console.out "$CONSOLE_FIFO" mkfifo "$CONSOLE_FIFO" # The reader holds the FIFO open so the script's short-lived `>` opens never block or EOF it. ( while :; do cat "$CONSOLE_FIFO" >> /work/console.out 2>/dev/null || true; done ) & CONSOLE_CAT_PID=$! : > /work/console.out } console_settle() { sleep 0.2; } # let the reader drain before a check reads the file say() { echo "TEST: $*"; } check() { if eval "$2"; then echo " ok: $1"; else echo " FAIL: $1"; fail=1; fi; } # --- fake sleep: counts waits; flips the poll to 200 after 3, hard-aborts a runaway loop. The gate # scenarios (G3/G4) use /work/sleep.abort to end the deliberately-infinite screen+retry loop. ---- cat > "$FAKE/sleep" <<'SLEEP' #!/bin/bash n=$(cat /work/sleep.count 2>/dev/null || echo 0); n=$((n+1)); echo "$n" > /work/sleep.count flip=$(cat /work/sleep.flip 2>/dev/null || echo "") [ -n "$flip" ] && [ "$n" -ge "$flip" ] && echo 200 > /work/poll-mode abort=$(cat /work/sleep.abort 2>/dev/null || echo 25) # abort only on WAIT-length sleeps (>=30s: the poll/retry waits) — never the gate's 2s diag settle, # which runs inside a command substitution: killing that subshell would blank the last screen paint. if [ "$n" -gt "$abort" ] && [ "${1:-0}" -ge 30 ]; then echo "ABORT: fake sleep hit $n calls — killing the loop" >&2; kill -TERM $PPID fi exit 0 SLEEP chmod +x "$FAKE/sleep" # --- gate fakes: ip / ifreload / dhclient log their calls; dhclient rc comes from /work/dhcp-mode --- cat > "$FAKE/ip" <<'IP' #!/bin/bash echo "$*" >> /work/ip.log exit 0 IP cat > "$FAKE/ifreload" <<'IFR' #!/bin/bash echo "$*" >> /work/ifreload.log exit 0 IFR cat > "$FAKE/dhclient" <<'DH' #!/bin/bash echo "$*" >> /work/dhclient.log exit "$(cat /work/dhcp-mode 2>/dev/null || echo 0)" DH chmod +x "$FAKE/ip" "$FAKE/ifreload" "$FAKE/dhclient" # --- fake curl: logs every invocation's URL; emulates -o (fetch), --data (register), -w code (poll) -- cat > "$FAKE/curl" <<'CURL' #!/bin/bash url=""; ofile=""; wfmt="" prev="" for a in "$@"; do case "$a" in http*|https*) url="$a";; esac case "$prev" in -o) ofile="$a";; -w) wfmt="$a";; esac prev="$a" done echo "$url" >> /work/curl.log mode=$(cat /work/poll-mode 2>/dev/null || echo 204) case "$url" in *"hub.example/") # the network gate's reachability probe (-o /dev/null -w '%{http_code}'). # /work/hub-mode: absent|up -> 302; down -> 000/exit 7; follow: -> up only when the # interfaces fixture has bridge-ports (the sweep's "this NIC reaches the hub" oracle). hm=$(cat /work/hub-mode 2>/dev/null || echo up) case "$hm" in follow:*) if grep -q "bridge-ports ${hm#follow:}\$" /work/interfaces 2>/dev/null; then hm=up; else hm=down; fi ;; esac if [ "$hm" = "up" ]; then printf '302'; exit 0; else printf '000'; exit 7; fi ;; esac case "$url" in *"/felhom-host-install.sh") # write a stub host-install to the -o target cat > "$ofile" <<'HI' #!/bin/bash echo "fake host-install ran: $*" >> /work/hostinstall.log exit 0 HI exit 0 ;; *"/appliance/register") # pairing_code (R-27): without it print_pairing_banner returns early and the banner is never # painted — which is how the banner went untested until v1.27.0 (R-496). echo '{"appliance_token":"TESTTOKEN123456","poll_interval_sec":30,"pairing_code":"TST-CDE"}' exit 0 ;; *"/appliance/poll") if [ "$mode" = "200" ]; then # body then, if -w set, a newline + code (matches the bootstrap's -w '\n%{http_code}') printf '%s' '{"customer_id":"drill","retrieval_passphrase":"SEKRET-PASS","mode":"appliance","extra_args":"--cores 2"}' [ -n "$wfmt" ] && printf '\n200' else [ -n "$wfmt" ] && printf '\n%s' "$mode" # 204 / 410 / whatever the scenario set fi exit 0 ;; esac exit 0 CURL chmod +x "$FAKE/curl" # fake systemctl (disable is a no-op); logs every call so R-496's pvebanner mask is observable printf '#!/bin/bash\necho "$*" >> /work/systemctl.log\nexit 0\n' > "$FAKE/systemctl"; chmod +x "$FAKE/systemctl" reset_state() { rm -rf /etc/felhom /run/felhom-bootstrap-pass /var/lib/felhom-install "$CALLS" /work/hostinstall.log \ /work/poll-mode /work/sleep.count /work/sleep.flip /work/sleep.abort \ /work/ip.log /work/ifreload.log /work/dhclient.log /work/hub-mode /work/dhcp-mode \ /work/sys /work/interfaces /work/interfaces.felhom-bak /work/console.out \ /run/felhom-interfaces.orig /work/run.log /work/issue /work/systemctl.log mkdir -p /etc/felhom } # gate fixture: two "physical" NICs (nicA = the configured bridge-port, nicB = the other one) and an # interfaces file in the installer-fallback shape (static 192.168.100.2 on vmbr0 -> nicA). make_gate_fixture() { mkdir -p /work/sys/nicA /work/sys/nicB /work/sys/vmbr0 /work/sys/lo touch /work/sys/nicA/device /work/sys/nicB/device # only these two count as physical echo "aa:aa:aa:aa:aa:0a" > /work/sys/nicA/address; echo 1 > /work/sys/nicA/carrier; echo 1000 > /work/sys/nicA/speed echo "bb:bb:bb:bb:bb:0b" > /work/sys/nicB/address; echo 1 > /work/sys/nicB/carrier; echo -1 > /work/sys/nicB/speed cat > /work/interfaces <<'IFACES' auto lo iface lo inet loopback auto vmbr0 iface vmbr0 inet static address 192.168.100.2/24 bridge-ports nicA bridge-stp off bridge-fd 0 iface nicA inet manual iface nicB inet manual source /etc/network/interfaces.d/* IFACES } GATE_ENV="FELHOM_NET_SYS=/work/sys FELHOM_INTERFACES_FILE=/work/interfaces FELHOM_CONSOLE_DEV=/work/console.out FELHOM_ISSUE_FILE=/work/issue" # ============================ Scenario D — direct mode, zero appliance calls ========================= # Runs WITH the gate fixture (NICs + fallback-shaped interfaces) and the hub reachable: the G1 # invariant below is only meaningful if the gate HAD candidates to touch and touched none. say "D: direct env -> run_direct, NO appliance calls" reset_state cat > /etc/felhom/bootstrap.env <<'ENV' FELHOM_CUSTOMER_ID=acme FELHOM_MODE=appliance FELHOM_RETRIEVAL_PASSPHRASE=direct-pass FELHOM_HUB_URL=https://hub.example ENV chmod 0600 /etc/felhom/bootstrap.env make_gate_fixture env $GATE_ENV bash "$BSTRAP"; rc=$? check "run_direct exited 0 (host-install stub succeeded)" "[ $rc -eq 0 ]" check "host-install was invoked" "[ -f /work/hostinstall.log ]" check "ZERO /appliance/register calls" "! grep -q '/appliance/register' $CALLS" check "ZERO /appliance/poll calls" "! grep -q '/appliance/poll' $CALLS" check "done-flag written" "[ -f /etc/felhom/.bootstrap-done ]" check "env shredded on success" "[ ! -f /etc/felhom/bootstrap.env ]" # G1 (zero-new-behavior invariant): with the hub reachable, the gate returned before ANY network # tooling was touched — no ip, no ifreload, no dhclient call exists. check "G1: gate made zero ip calls" "[ ! -f /work/ip.log ]" check "G1: gate made zero ifreload calls" "[ ! -f /work/ifreload.log ]" check "G1: gate made zero dhclient calls" "[ ! -f /work/dhclient.log ]" check "G1: gate consumed zero sleeps" "[ ! -f /work/sleep.count ]" check "G1: interfaces fixture untouched" "grep -q 'bridge-ports nicA' /work/interfaces && grep -q '192.168.100.2' /work/interfaces" # R-496 (v1.27.0): the console's login banner is Felhom's, not Proxmox's — and it survives a reboot, # because pvebanner.service (which rewrites /etc/issue on EVERY boot) is masked, not merely overwritten. check "R-496: /etc/issue written" "[ -s /work/issue ]" check "R-496: /etc/issue is the Felhom text" "grep -q 'Felhom otthoni szerver' /work/issue" check "R-496: /etc/issue carries no admin URL (:8006)" "! grep -q '8006' /work/issue" check "R-496: /etc/issue does not say Proxmox" "! grep -qi 'proxmox' /work/issue" check "R-496: pvebanner.service masked" "grep -q 'mask pvebanner.service' /work/systemctl.log" # v1.27.1: the login screen is painted BEFORE the Latin-2 console font loads, so ő/ű in /etc/issue # rendered as dropped letters on the 1.27.0 proof install („képernyon", „teendod", screen 331-s20). check "R-496: /etc/issue carries no ő/ű (the boot font lacks them)" "! grep -q '[őűŐŰ]' /work/issue" # ============ P: pairing loop (v1.21.0) — register, wait unbound INSIDE one invocation, deliver ===== say "P: pairing env -> register + in-script 204 wait -> 200 delivery -> host-install, ONE invocation" reset_state cat > /etc/felhom/bootstrap.env <<'ENV' FELHOM_HUB_URL=https://hub.example ENV echo 204 > /work/poll-mode echo 3 > /work/sleep.flip # after 3 in-script waits the hub "binds" (poll flips to 200) console_reset env FELHOM_CONSOLE_DEV="$CONSOLE_FIFO" FELHOM_ISSUE_FILE=/work/issue bash "$BSTRAP"; rc=$? console_settle # R-496: the pairing banner names the secret the way the self-bind mail and page do (R-323). check "R-496: banner painted to the console seam" "grep -q 'Párosító kód' /work/console.out" check "R-496: banner names the Tulajdonosi jelmondat" "grep -q 'Tulajdonosi jelmondat' /work/console.out" check "R-496: banner no longer says 'jelszavad'" "! grep -q 'jelszavad' /work/console.out" # ===== R-559 (slice 4): the console is BILINGUAL — Hungarian frozen, English real, both fit 80 cols ===== # # THE HUNGARIAN IS A GOLDEN, NOT A GREP. /work/golden/*.hu.txt were captured from the script at # 183727db9c44, before a single English line was added. A grep for one phrase would pass a banner # whose other nine lines had been reworded; the golden is the whole block, byte for byte. # # The golden is the banner WITHOUT its closing frame line, because the English block is appended # INSIDE the frame and the closing line therefore moves down. So the assertion is: the banner's first # N lines are exactly the golden, where N is the golden's own length. split_banners() { # $1 = the console capture. Writes /work/b.pairing and /work/b.bound, each the whole banner from # its opening frame line to its closing one inclusive. Frames are counted, not guessed: the # capture holds exactly two banners and therefore four frame lines. awk -v out=/work/b ' /^=+$/ { f++ name = (f<=2 ? "pairing" : "bound") print > (out "." name) next } { if (f==1 || f==3) print > (out "." (f==1 ? "pairing" : "bound")) } ' "$1" } hu_head_matches() { # $1 = banner file, $2 = golden file local n; n=$(wc -l < "$2") head -n "$n" "$1" | diff -u "$2" - >/dev/null } split_banners /work/console.out for b in pairing bound; do check "R-559: the $b banner's Hungarian block is byte-identical to the golden" \ "hu_head_matches /work/b.$b /work/golden/$b.hu.txt" # The English block is everything after the Hungarian block, above the closing frame. n=$(wc -l < "/work/golden/$b.hu.txt") tail -n +$((n+1)) "/work/b.$b" | grep -v '^={40,}$' > "/work/en.$b" check "R-559: the $b English block exists" "[ -s /work/en.$b ]" # ASCII-fragment search with BOTH controls (rule 9.8): the English block must carry no Hungarian # letter, and the Hungarian block must carry one — or the check is matching nothing at all. check "R-559: the $b English block has no Hungarian letter" "! grep -q '[áéíóöőúüűÁÉÍÓÖŐÚÜŰ]' /work/en.$b" check "R-559: CONTROL — the $b Hungarian block does have one" "grep -q '[áéíóöőúüűÁÉÍÓÖŐÚÜŰ]' /work/golden/$b.hu.txt" # 80 columns is the console's width, counted in CHARACTERS not bytes: the Hungarian lines are # multi-byte, so `wc -c` would convict them wrongly. check "R-559: every $b line fits 80 columns" \ "[ \"\$(awk '{ print length(\$0) }' /work/b.$b | sort -rn | head -1)\" -le 80 ]" done # The pairing code appears once per language — a person reads one block, and must find it there. check "R-559: the pairing code appears in BOTH blocks" \ "[ \"\$(grep -c 'TST-CDE' /work/b.pairing)\" -eq 2 ]" # HEIGHT, not just width — the measurement slice 4's plan called for, and the one a width check # cannot make. A console is 80x25. The pairing banner is now 21 framed lines plus the blank before it # and the two after: 24 rows, and the console shows the LAST 25. One more line and the paint is 25; # two more and the top row scrolls away — and the top of this banner is where the HUNGARIAN pairing # code sits (row 5). The banner exists to be read; a banner whose code has scrolled off is furniture. # # 25 is the limit, not 24, because 25 still fits exactly. The margin is one line and this check is # what makes that fact survive the next person adding a sentence. banner_rows() { echo $(( $(wc -l < "$1") + 3 )); } # +1 leading blank, +2 trailing check "R-559: the pairing banner fits a 25-row console (it is $(banner_rows /work/b.pairing))" \ "[ \"\$(banner_rows /work/b.pairing)\" -le 25 ]" check "R-559: the bound banner fits a 25-row console (it is $(banner_rows /work/b.bound))" \ "[ \"\$(banner_rows /work/b.bound)\" -le 25 ]" check "R-559: the bound banner carries no pairing code" "! grep -q 'TST-CDE' /work/b.bound" # /etc/issue: the Hungarian half frozen, the English half added, still no ő/ű and still no admin URL. check "R-559: /etc/issue still opens with the golden Hungarian" \ "head -n \$(wc -l < /work/golden/issue.hu.txt) /work/issue | diff -q /work/golden/issue.hu.txt - >/dev/null" check "R-559: /etc/issue has an English half" "grep -q 'Felhom home server' /work/issue" check "R-559: /etc/issue English half has no Hungarian letter" \ "! sed -n '/Felhom home server/,\$p' /work/issue | grep -q '[áéíóöőúüűÁÉÍÓÖŐÚÜŰ]'" check "R-559: /etc/issue names no admin URL" "! grep -q '8006' /work/issue" check "single invocation ran to done (exit 0)" "[ $rc -eq 0 ]" check "POSTed /appliance/register" "grep -q '/appliance/register' $CALLS" check "appliance token persisted 0600" "[ -f /etc/felhom/.bootstrap-done ] || { [ -f /etc/felhom/appliance-token ] && [ \"\$(stat -c %a /etc/felhom/appliance-token)\" = 600 ]; }" check "polled more than once (the wait lived in-script, not in systemd restarts)" "[ \$(grep -c '/appliance/poll' $CALLS) -ge 2 ]" check "waited between polls (fake sleep called >=3x)" "[ \"\$(cat /work/sleep.count 2>/dev/null || echo 0)\" -ge 3 ]" check "host-install invoked after delivery" "[ -f /work/hostinstall.log ]" check "done-flag written" "[ -f /etc/felhom/.bootstrap-done ]" # the token was ALSO consumed on success (run_direct scrubs both secrets) check "env shredded on success" "[ ! -f /etc/felhom/bootstrap.env ]" # ============ 410: consumed delivery without a local env -> exit non-zero (crash window) ============ say "410: consumed delivery + no env -> exit non-zero so systemd restarts clean" reset_state cat > /etc/felhom/bootstrap.env <<'ENV' FELHOM_HUB_URL=https://hub.example ENV echo 410 > /work/poll-mode bash "$BSTRAP"; rc=$? check "410 poll exits non-zero" "[ $rc -ne 0 ]" check "host-install NOT run" "[ ! -f /work/hostinstall.log ]" # ============ G2: sweep-success — re-point to the hub-reaching NIC, persist, proceed ================ say "G2: hub down + no state.json -> sweep finds nicB, persists (bak kept), proceeds to pairing" reset_state cat > /etc/felhom/bootstrap.env <<'ENV' FELHOM_HUB_URL=https://hub.example ENV make_gate_fixture echo "follow:nicB" > /work/hub-mode # hub reachable ONLY once vmbr0 points at nicB echo 410 > /work/poll-mode # after the gate, exit the pairing loop promptly env $GATE_ENV bash "$BSTRAP" > /work/run.log 2>&1; rc=$? check "sweep re-pointed vmbr0 to nicB" "grep -q 'bridge-ports nicB' /work/interfaces" check "winner persisted with DHCP addressing" "grep -q 'iface vmbr0 inet dhcp' /work/interfaces" check "fallback static address dropped" "! grep -q '192.168.100.2' /work/interfaces" check "original saved as interfaces.felhom-bak" "[ -f /work/interfaces.felhom-bak ] && grep -q 'bridge-ports nicA' /work/interfaces.felhom-bak" check "self-heal logged loudly" "grep -q 'network self-heal: vmbr0 -> nicB' /work/run.log" check "proceeded to pairing after the heal" "grep -q '/appliance/register' $CALLS" check "sweep exercised ifreload" "[ -f /work/ifreload.log ]" # ============ G3: sweep-fail — screen painted, interfaces BYTE-IDENTICAL ============================ say "G3: hub down everywhere -> console screen painted, interfaces byte-identical, no bak" reset_state cat > /etc/felhom/bootstrap.env <<'ENV' FELHOM_HUB_URL=https://hub.example ENV make_gate_fixture echo down > /work/hub-mode echo 1 > /work/dhcp-mode # no NIC leases either echo 8 > /work/sleep.abort # end the deliberate screen+retry loop after a few cycles PRE_SHA=$(sha256sum /work/interfaces | awk '{print $1}') env $GATE_ENV bash "$BSTRAP" > /work/run.log 2>&1; rc=$? POST_SHA=$(sha256sum /work/interfaces | awk '{print $1}') check "console screen painted (header)" "grep -q 'Nincs hálózati kapcsolat' /work/console.out" check "screen lists the NICs" "grep -q 'nicA' /work/console.out && grep -q 'nicB' /work/console.out" check "screen names the fallback signature" "grep -q '192.168.100.2' /work/console.out" check "screen carries the remedy line" "grep -q 'percenként újra próbálkozik' /work/console.out" check "interfaces BYTE-IDENTICAL after failed sweep" "[ \"$PRE_SHA\" = \"$POST_SHA\" ]" check "no .felhom-bak on failure (success-only persist)" "[ ! -f /work/interfaces.felhom-bak ]" check "host-install never ran" "[ ! -f /work/hostinstall.log ]" # ============ G4: state.json present — screen only, the sweep is NEVER invoked ====================== say "G4: hub down + state.json present -> screen + retry, sweep NEVER invoked, interfaces untouched" reset_state cat > /etc/felhom/bootstrap.env <<'ENV' FELHOM_HUB_URL=https://hub.example ENV make_gate_fixture mkdir -p /var/lib/felhom-install && touch /var/lib/felhom-install/state.json echo down > /work/hub-mode echo 8 > /work/sleep.abort PRE_SHA=$(sha256sum /work/interfaces | awk '{print $1}') env $GATE_ENV bash "$BSTRAP" > /work/run.log 2>&1; rc=$? POST_SHA=$(sha256sum /work/interfaces | awk '{print $1}') check "sweep NEVER invoked: zero ifreload calls" "[ ! -f /work/ifreload.log ]" check "sweep NEVER invoked: zero dhclient calls" "[ ! -f /work/dhclient.log ]" check "interfaces untouched" "[ \"$PRE_SHA\" = \"$POST_SHA\" ]" check "console screen still painted" "grep -q 'Nincs hálózati kapcsolat' /work/console.out" check "no-sweep branch logged" "grep -q 'no sweep, interfaces untouched' /work/run.log" # ============ PI: the package postinst (v1.27.1) — the FIRST boot must already be Felhom's =========== # Measured on the 1.27.0 proof install (screen 331-s20): pvebanner.service ran at the first boot BEFORE # felhom-bootstrap could mask it, so the household's first screen still carried the Proxmox admin URL. # The postinst runs in the installer chroot (no systemd): it may only write files. It masks by symlink. say "PI: postinst configure -> pvebanner masked by symlink, /etc/issue is Felhom's, exit 0" rm -f /etc/systemd/system/pvebanner.service /etc/issue /work/systemctl.log printf '\nWelcome to the Proxmox Virtual Environment ... https://10.0.0.1:8006/\n' > /etc/issue sh /work/postinst configure; rc=$? check "PI: postinst exits 0" "[ $rc -eq 0 ]" check "PI: pvebanner.service -> /dev/null (masked by file)" "[ \"\$(readlink /etc/systemd/system/pvebanner.service)\" = /dev/null ]" check "PI: /etc/issue is the Felhom text" "grep -q 'Felhom otthoni szerver' /etc/issue" check "PI: /etc/issue carries no admin URL" "! grep -q '8006' /etc/issue" check "PI: /etc/issue carries no ő/ű" "! grep -q '[őűŐŰ]' /etc/issue" check "PI: postinst and bootstrap write the SAME issue text" "cmp -s /etc/issue /work/issue" echo "==================================================" if [ $fail -eq 0 ]; then echo "ALL BOOTSTRAP-MODE TESTS PASSED"; else echo "SOME TESTS FAILED"; fi exit $fail