diff --git a/scripts/iso/felhom-bootstrap.sh b/scripts/iso/felhom-bootstrap.sh index c448fee..ff9752a 100644 --- a/scripts/iso/felhom-bootstrap.sh +++ b/scripts/iso/felhom-bootstrap.sh @@ -27,6 +27,14 @@ # — its producer steps re-run every pass. FIRST direct attempt is plain; any later attempt that finds # the install state file adds --resume. State file: /var/lib/felhom-install/state.json. # +# R-59/R-60 (v1.24.0) — the FIRST-BOOT NETWORK GATE runs before mode dispatch: if the hub is +# unreachable, the box refuses to wait silently. It diagnoses (physical-NIC table, installer +# 192.168.100.2-fallback signature), SWEEPS the NICs while no install attempt has begun (re-point +# vmbr0 -> bounded DHCP -> hub probe; mechanics + bounds measured in +# documentation/audits/SPIKE-firstboot-nic-sweep-2026-07-22.md), and otherwise paints a legible +# Hungarian screen on the console and retries every minute — the unit stays `activating` and never +# exits non-zero while waiting (the v1.21.0 lesson: waiting is not failing). +# # NOT production-generic: this is the R-21 bare-metal first-boot bootstrap. It does NOT modify # felhom-host-install.sh; it only invokes it. #=============================================================================== @@ -86,6 +94,191 @@ fi HUB_URL="${FELHOM_HUB_URL:-https://hub.felhom.eu}" INSTALL_URL="${FELHOM_INSTALL_URL:-https://felhom.eu/scripts/felhom-host-install.sh}" +# ===================================================================================================== +# R-59/R-60 — the first-boot network gate (v1.24.0). Design inputs: SPIKE-firstboot-nic-sweep +# (F-P1..F-P8). The TRIGGER is always "hub unreachable" — never the 192.168.100.2 signature (a +# wrong-NIC box also exists as "leased at install, truth changed after": plausible static config, +# no fallback signature). The signature is diagnosis detail for the log + screen. +# ===================================================================================================== +# Seams (the PATH-fake harness points these at fixtures; production = the defaults): +NET_SYS="${FELHOM_NET_SYS:-/sys/class/net}" +INTERFACES_FILE="${FELHOM_INTERFACES_FILE:-/etc/network/interfaces}" +CONSOLE_DEV="${FELHOM_CONSOLE_DEV:-/dev/console}" +GATE_RETRY_INTERVAL=60 # the screen promises "a doboz percenként újra próbálkozik" +SWEEP_DHCP_TIMEOUT=20 # F-P1/F-P4: a real lease lands in ~3s; a dead NIC never returns on its own +HUB_PROBE_TIMEOUT=10 +GATE_ORIG_COPY=/run/felhom-interfaces.orig + +hub_reachable() { + # F-P5: ANY HTTP status proves TLS+HTTP reachability (the hub answers / with a 302); only + # 000/empty means no contact. + local code + code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time "$HUB_PROBE_TIMEOUT" "$HUB_URL/" 2>/dev/null) + [[ -n "$code" && "$code" != "000" ]] +} + +physical_nics() { + # Physical NICs only — same rule as gather_identity_json, plus the explicit name excludes. + local d n + for d in "$NET_SYS"/*; do + [[ -e "$d" ]] || continue + n=$(basename "$d") + case "$n" in lo|vmbr*|veth*|tap*|fwln*|fwpr*) continue ;; esac + [[ -e "$d/device" ]] || continue + echo "$n" + done +} + +nic_diag_table() { + # F-P2: unused NICs sit admin-DOWN and their carrier is unreadable while down — raise them + # first, settle once, then read. (The sweep itself needs none of this: ifreload raises the + # configured port on its own. This is for the human-facing table.) + local n raised=0 + for n in $(physical_nics); do ip link set "$n" up 2>/dev/null && raised=1; done + [[ $raised -eq 1 ]] && sleep 2 + local mac carrier speed cable + for n in $(physical_nics); do + mac=$(cat "$NET_SYS/$n/address" 2>/dev/null || echo '?') + carrier=$(cat "$NET_SYS/$n/carrier" 2>/dev/null || echo '') + speed=$(cat "$NET_SYS/$n/speed" 2>/dev/null || echo '') + case "$carrier" in 1) cable="van";; 0) cable="nincs";; *) cable="?";; esac + if [[ "$speed" =~ ^[0-9]+$ ]]; then speed="${speed} Mb/s"; else speed="?"; fi + printf ' %-12s %-18s kábel: %-6s %s\n' "$n" "$mac" "$cable" "$speed" + done +} + +fallback_signature_present() { + # The installer's no-DHCP fallback baked as static (the demo-hp shape, R-59). + grep -Eq '^[[:space:]]*address[[:space:]]+192\.168\.100\.2(/|[[:space:]]|$)' "$INTERFACES_FILE" 2>/dev/null +} + +current_bridge_port() { + awk '/^auto vmbr0$/{f=1} f && /bridge-ports/{print $2; exit}' "$INTERFACES_FILE" 2>/dev/null +} + +render_candidate_interfaces() { + # Probe shape for candidate $1: ONLY bridge-ports changes, always derived from the PRISTINE + # copy (candidates never stack). The static stanza stays — dhclient simply adds the leased + # address next to it (proven in the spike), so a working static survives the probe untouched. + sed -E "s/^([[:space:]]*bridge-ports[[:space:]]+).*/\1$1/" "$GATE_ORIG_COPY" +} + +persist_winner_interfaces() { + # F-P7: the winner is by definition a leasing NIC — persist bridge-ports + DHCP addressing. + # The static lines are dropped: in the fallback variant they are garbage (192.168.100.2), and + # in the truth-changed variant DHCP re-acquires the same router anyway. + awk -v nic="$1" ' + /^iface vmbr0 inet/ { print "iface vmbr0 inet dhcp"; invmbr=1; next } + /^(iface|auto|source)/ { invmbr=0 } + invmbr && /^[[:space:]]+(address|gateway)[[:space:]]/ { next } + { if (invmbr && $1=="bridge-ports") $0="\tbridge-ports " nic; print } + ' "$GATE_ORIG_COPY" +} + +sweep_nics() { + # R-60. The caller guarantees BOTH gates: hub unreachable AND no install state file — never + # re-shuffle NICs once an install attempt has begun. Structurally this whole path is first-boot + # only twice over: the state-file gate here, and the unit's + # ConditionPathExists=!/etc/felhom/.bootstrap-done confining the entire script to the + # pre-done lifetime. + cp -a "$INTERFACES_FILE" "$GATE_ORIG_COPY" || return 1 + + local cur n + cur=$(current_bridge_port) + # Candidate order: the current port first (a fresh lease on the same port heals a + # subnet/addressing change), then the rest. F-P3: carrier only orders, DHCP + hub-probe decide. + local ordered=() + [[ -n "$cur" ]] && ordered+=("$cur") + for n in $(physical_nics); do [[ "$n" == "$cur" ]] || ordered+=("$n"); done + [[ ${#ordered[@]} -gt 0 ]] || { rm -f "$GATE_ORIG_COPY"; return 1; } + + for n in "${ordered[@]}"; do + log "network sweep: trying vmbr0 -> $n (bounded DHCP ${SWEEP_DHCP_TIMEOUT}s + hub probe)" + render_candidate_interfaces "$n" > "${INTERFACES_FILE}.felhom-tmp" \ + && mv "${INTERFACES_FILE}.felhom-tmp" "$INTERFACES_FILE" + ifreload -a 2>/dev/null + timeout "$SWEEP_DHCP_TIMEOUT" dhclient -1 vmbr0 2>/dev/null + local drc=$? + if [[ $drc -eq 0 ]] && hub_reachable; then + local mac; mac=$(cat "$NET_SYS/$n/address" 2>/dev/null || echo '?') + # SUCCESS-ONLY persist (atomic tmp+mv; the original survives as interfaces.felhom-bak). + cp -a "$GATE_ORIG_COPY" "${INTERFACES_FILE}.felhom-bak" + persist_winner_interfaces "$n" > "${INTERFACES_FILE}.felhom-tmp" \ + && mv "${INTERFACES_FILE}.felhom-tmp" "$INTERFACES_FILE" + log "network self-heal: vmbr0 -> $n ($mac), hub reachable" + rm -f "$GATE_ORIG_COPY" + return 0 + fi + pkill -x dhclient 2>/dev/null # F-P4: clear the failed/killed client before the next candidate + done + + # Every candidate failed -> restore the pristine config BYTE-IDENTICALLY. A failed sweep must + # never leave a half-rewritten config behind (harness red-proofs this guard). + cp -a "$GATE_ORIG_COPY" "${INTERFACES_FILE}.felhom-tmp" && mv "${INTERFACES_FILE}.felhom-tmp" "$INTERFACES_FILE" + ifreload -a 2>/dev/null + rm -f "$GATE_ORIG_COPY" + return 1 +} + +paint_network_screen() { + # R-59: the legible refuse-loudly screen (print_pairing_banner pattern — /dev/console, stdout + # fallback). Calm adult Hungarian; spec-fixed copy. + local table="$1" fbnote="$2" + { printf '\n================================================\n' + printf ' Felhom — Nincs hálózati kapcsolat: a doboz nem éri\n' + printf ' el a felhom.eu szolgáltatást.\n\n' + printf ' Hálózati csatolók:\n' + printf ' %-12s %-18s %-13s %s\n' 'név' 'MAC' 'kábel' 'sebesség' + printf '%s\n' "$table" + if [[ -n "$fbnote" ]]; then + printf '\n A telepítéskor egyik porton sem volt élő kapcsolat,\n' + printf ' ezért a doboz a telepítő tartalék-címén (192.168.100.2) áll.\n' + fi + printf '\n Csatlakoztassa a hálózati kábelt egy másik portba, vagy\n' + printf ' ellenőrizze a routert — a doboz percenként újra próbálkozik.\n' + printf '================================================\n\n' + } > "$CONSOLE_DEV" 2>/dev/null \ + || printf 'felhom: nincs hálózati kapcsolat — a doboz percenként újra próbálkozik.\n' +} + +network_gate() { + # 1. Happy path FIRST, with ZERO new behavior: hub reachable -> return immediately. No ip, no + # dhclient, no ifreload, no interfaces read happens before this return (B'-style invariant, + # asserted by the harness). + if hub_reachable; then + return 0 + fi + log "network gate: hub unreachable ($HUB_URL) — diagnosing (R-59)" + local table fbnote + while true; do + table=$(nic_diag_table) + fbnote="" + if fallback_signature_present; then + fbnote=yes + log "network gate: installer fallback signature (static 192.168.100.2 on vmbr0) — no NIC leased at install time" + fi + log "network gate: NIC status:"$'\n'"$table" + if [[ ! -e "$STATE_FILE" ]]; then + if sweep_nics; then + return 0 + fi + log "network gate: sweep found no NIC that reaches the hub — console screen + retry every ${GATE_RETRY_INTERVAL}s" + else + # An install attempt exists: NEVER re-shuffle NICs under it — screen + retry only. + log "network gate: install already attempted ($STATE_FILE present) — no sweep, interfaces untouched; console screen + retry" + fi + paint_network_screen "$table" "$fbnote" + # Waiting is not failing (v1.21.0): the unit stays `activating`; we never exit non-zero + # here. A moved cable heals on the next cycle — via the plain probe (same port) or the + # sweep (different port). + sleep "$GATE_RETRY_INTERVAL" + if hub_reachable; then + log "network gate: hub reachable — proceeding" + return 0 + fi + done +} + # ===================================================================================================== # DIRECT mode — fetch + run host-install with the customer passphrase (slice A, unchanged behaviour). # ===================================================================================================== @@ -306,6 +499,9 @@ emit("FELHOM_EXTRA_ARGS", d.get("extra_args")) done } +# --- R-59/R-60 first-boot network gate: never proceed silently into a hub-unreachable install ------ +network_gate + # --- mode selection ------------------------------------------------------------------------------- if [[ -n "$FELHOM_CUSTOMER_ID" && -n "$FELHOM_RETRIEVAL_PASSPHRASE" ]]; then run_direct diff --git a/scripts/iso/test/bootstrap-modes.sh b/scripts/iso/test/bootstrap-modes.sh index 695359a..1a16b22 100644 --- a/scripts/iso/test/bootstrap-modes.sh +++ b/scripts/iso/test/bootstrap-modes.sh @@ -9,6 +9,15 @@ # 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 @@ -20,17 +29,41 @@ fail=0 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 ------------ +# --- 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 -if [ "$n" -gt 25 ]; then echo "RUNAWAY: fake sleep hit $n calls — killing the loop" >&2; kill -TERM $PPID; fi +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 @@ -43,6 +76,17 @@ for a in "$@"; do 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 @@ -74,11 +118,43 @@ printf '#!/bin/bash\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/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 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" + # ============================ 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' @@ -88,13 +164,21 @@ FELHOM_RETRIEVAL_PASSPHRASE=direct-pass FELHOM_HUB_URL=https://hub.example ENV chmod 0600 /etc/felhom/bootstrap.env -bash "$BSTRAP"; rc=$? +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" # ============ 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" @@ -126,6 +210,64 @@ 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" + echo "==================================================" if [ $fail -eq 0 ]; then echo "ALL BOOTSTRAP-MODE TESTS PASSED"; else echo "SOME TESTS FAILED"; fi exit $fail