eb1ae37095
gates / gates (push) Successful in 23s
THE IMAGE IS NOT BUILT AND NOT PUBLISHED BY THIS COMMIT. Publishing to iso.felhom.eu is public and irreversible and its runbook requires a proof install on BOTH menu entries plus the 16-criterion gate run against the exact uploaded bytes. That is the operator's step. The download pages therefore still name 1.28.0 - the image that is actually published - and a new site gate refuses the two pages naming different files or hashes. Three texts a person meets before any dashboard become bilingual: Hungarian block first, byte for byte as before, then English, inside the same frame. The pairing banner, the bound banner, /etc/issue (and the postinst's byte-coupled copy), plus an English half on the GRUB entries. The Hungarian is a GOLDEN, not a grep: test/golden/*.hu.txt were captured from the script at183727db9cbefore one English line existed, and the harness asserts each banner's first N lines are exactly the golden. Red-proofed by one changed byte, by an "a" planted in the English block, and by an over-wide line. R-586, found on the way in: running the harness UNCHANGED at the base commit failed two R-496 checks. The script paints with `>`, which truncates a FILE but is a no-op on a console device; ISO 1.28.0's new bound banner (c033b3b) paints straight after the pairing one and wiped it before the check read it.c033b3bdid not touch the harness, and nobody saw it because the harness is in no gate and no CI run. Fixed with a FIFO; production code untouched. The harness being ungated is still open. The release gate's G16 required every Felhom string to be Hungarian and would have STOPPED this publication. Operator ruling 1b of 2026-09-17 supersedes that scope, so G16 is rewritten rather than waived: Hungarian FIRST, pinned by the golden, each secret named once per language. letoltes.html changes by four lines only. The English link is not in the nav - the nav is a shared block site_gates.py pins across every page, and the gate convicted the first attempt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
388 lines
22 KiB
Bash
388 lines
22 KiB
Bash
#!/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:<nic> -> up only when the
|
|
# interfaces fixture has bridge-ports <nic> (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 ]"
|
|
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
|