Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
438 lines
28 KiB
Bash
Executable File
438 lines
28 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Static checks for the host-config templating. No root needed.
|
|
# ./tests/check-templates.sh
|
|
set -euo pipefail
|
|
cd "$(dirname "$(readlink -f "$0")")/.."
|
|
|
|
fail=0
|
|
ok() { echo "ok - $*"; }
|
|
bad() { echo "FAIL - $*"; fail=1; }
|
|
|
|
# 1. shell syntax
|
|
for f in deploy.sh failover/sync-usb-backup.sh failover/sync-sd-backup.sh tests/check-templates.sh; do
|
|
if bash -n "$f"; then ok "bash -n $f"; else bad "bash -n $f"; fi
|
|
done
|
|
|
|
# 3. every host file passes deploy.sh --check (validates vars, renders all templates)
|
|
for c in hosts/*.conf; do
|
|
if out=$(./deploy.sh --check "$c" 2>&1); then
|
|
ok "--check $c"
|
|
else
|
|
bad "--check $c"; echo "$out" | sed 's/^/ /'
|
|
fi
|
|
done
|
|
|
|
# 4. the example defines exactly the variables deploy.sh knows about
|
|
known=$(sed -n '/^REQUIRED_VARS=(/,/^)/p;/^OPTIONAL_VARS=(/,/^)/p' deploy.sh \
|
|
| grep -vE '^[A-Z_]+=\(|^\)' | tr -s ' \t' '\n' | grep -v '^$' | sort || true)
|
|
defined=$(grep -oE '^[A-Z][A-Z0-9_]*=' hosts/vanlink.conf.example 2>/dev/null | tr -d = | sort || true)
|
|
if [ -n "$known" ] && [ "$known" = "$defined" ]; then
|
|
ok "hosts/vanlink.conf.example defines exactly the variables deploy.sh knows"
|
|
else
|
|
bad "hosts/vanlink.conf.example differs from deploy.sh's variable lists"
|
|
diff <(echo "$known") <(echo "$defined") | sed 's/^/ /' || true
|
|
fi
|
|
|
|
# 2. every file carrying an @TOKEN@ is installed through install_rendered
|
|
# (plain `install` would ship the placeholder unrendered — the 2026-09-23 failure)
|
|
# vanrouter.js: installed by the loop in the cockpit block; 50-van-wan.yaml: reference copy,
|
|
# rendered by hand via `deploy.sh render` (deploy.sh only warns when it differs)
|
|
# 50-van-wan-wifi.yaml, cellular.nmconnection: installed by dedicated code, not `install_rendered <src> /abs/dst` literal lines
|
|
exempt=" cockpit/vanrouter/vanrouter.js ap/50-van-wan.yaml ap/50-van-wan-wifi.yaml ap/cellular.nmconnection "
|
|
for f in $(./deploy.sh --templates); do
|
|
case "$exempt" in *" $f "*) continue ;; esac
|
|
if grep -qE "^[[:space:]]*install_rendered +$f[[:space:]]" deploy.sh; then
|
|
ok "install_rendered $f"
|
|
else
|
|
bad "$f contains an @TOKEN@ but deploy.sh does not install it with install_rendered"
|
|
fi
|
|
done
|
|
|
|
# 5. host-specific values land in the rendered output
|
|
r() { VANLINK_CONF="hosts/$1.conf" ./deploy.sh render "$2"; }
|
|
expect() { # expect <host> <file> <fixed string that must appear>
|
|
if r "$1" "$2" | grep -qF -- "$3"; then ok "$1: $2 has '$3'"; else bad "$1: $2 lacks '$3'"; fi
|
|
}
|
|
expect_line() { # expect_line <host> <file> <whole line that must appear>
|
|
if r "$1" "$2" | grep -qxF -- "$3"; then ok "$1: $2 has line '$3'"; else bad "$1: $2 lacks line '$3'"; fi
|
|
}
|
|
expect_absent() { # expect_absent <host> <file> <fixed string that must NOT appear>
|
|
if r "$1" "$2" | grep -qF -- "$3"; then bad "$1: $2 must not contain '$3'"; else ok "$1: $2 has no '$3'"; fi
|
|
}
|
|
expect van ap/nftables.conf 'ip saddr 10.42.1.0/24'
|
|
expect wan ap/nftables.conf 'ip saddr 10.42.0.0/24'
|
|
expect van ap/van-br0-setup.service 'brctl addif br0 enP1p17s0'
|
|
expect wan ap/van-br0-setup.service 'brctl addif br0 enx00e04c331140'
|
|
expect van failover/config.json '"device": ""'
|
|
expect wan failover/config.json '"device": "wlan0"'
|
|
expect van router-status/config.json '"wifi_wan_iface": ""'
|
|
expect van failover/van-wlan-watchdog.service 'ExecStart=/usr/local/sbin/van-wlan-watchdog'
|
|
expect wan failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0'
|
|
expect van ap/van-ap-unmanaged.conf 'interface-name:enP1p17s0'
|
|
expect wan ap/van-ap-unmanaged.conf 'interface-name:enx00e04c331140;interface-name:eth0'
|
|
expect wan ap/23-van-lan-eth0.network 'Name=eth0'
|
|
expect van power/nvme-watch-config.json '"nvme_device": ""'
|
|
expect wan ap/hostapd.conf 'interface=wlxc83a35a4ee55'
|
|
expect van ap/hostapd.conf 'interface=wlp1s0'
|
|
expect wan ap/hostapd-2g.conf 'interface=wlxf4f26d1760a7'
|
|
expect van ap/hostapd-2g.conf 'interface=wlx3c3332002066'
|
|
expect_line wan ap/hostapd.conf 'ssid=VanLink'
|
|
expect_line van ap/hostapd.conf 'ssid=VanLink2'
|
|
expect_line wan ap/hostapd-2g.conf 'ssid=VanLink'
|
|
expect_line van ap/hostapd-2g.conf 'ssid=VanLink2-2G'
|
|
expect_line wan ap/hostapd.conf 'vht_oper_chwidth=1'
|
|
expect_line wan ap/hostapd.conf 'he_oper_centr_freq_seg0_idx=155'
|
|
expect_line wan ap/hostapd.conf 'ieee80211h=0'
|
|
expect_line van ap/hostapd.conf 'he_su_beamformee=1'
|
|
expect_absent van ap/hostapd.conf 'vht_oper_chwidth'
|
|
if r van ap/van-ap-unmanaged.conf | grep -q 'eth0'; then
|
|
bad "van: unmanaged-devices must not mention eth0 (ONBOARD_LAN_IFACE is empty)"
|
|
else
|
|
ok "van: unmanaged-devices has no eth0"
|
|
fi
|
|
|
|
# 6. flagged units/paths only appear inside their ENABLE_* guard in deploy.sh.
|
|
# Convention: guards are column-0 `if [ "$ENABLE_X" = 1 ]; then` ... `fi`,
|
|
# contents indented (so a nested `fi` never sits in column 0).
|
|
if awk '
|
|
BEGIN {
|
|
need["van-li3-battery"]="ENABLE_LI3"
|
|
need["van-nvme-watch"]="ENABLE_NVME_WATCH"
|
|
need["nvme-watch-config"]="ENABLE_NVME_WATCH"
|
|
need["homeassistant"]="ENABLE_HA"
|
|
need["esphome"]="ENABLE_HA"
|
|
need["frigate"]="ENABLE_FRIGATE"
|
|
need["cockpit"]="ENABLE_COCKPIT"
|
|
}
|
|
/^if \[ "\$ENABLE_[A-Z0-9_]+" = 1 \]/ { match($0, /ENABLE_[A-Z0-9_]+/); guard=substr($0, RSTART, RLENGTH); next }
|
|
/^fi([[:space:]]|$)/ { guard=""; next }
|
|
/^[[:space:]]*(#|echo)/ { next }
|
|
{ for (u in need) if (index($0, u) && guard != need[u]) {
|
|
printf " deploy.sh:%d mentions %s outside an %s guard: %s\n", NR, u, need[u], $0; bad=1 } }
|
|
END { exit bad }
|
|
' deploy.sh; then ok "flagged units are only touched inside their ENABLE_* guard"; else bad "flag gating"; fi
|
|
|
|
# 7. backup scripts take their settings from the host config and say what is missing
|
|
empty=$(mktemp); trap 'rm -f "$empty"' EXIT
|
|
for pair in "failover/sync-usb-backup.sh:USB_BACKUP_ROOT_PART" "failover/sync-sd-backup.sh:SD_BACKUP_UUID"; do
|
|
s=${pair%%:*}; v=${pair##*:}
|
|
if out=$(VANLINK_CONF="$empty" "./$s" 2>&1); then
|
|
bad "$s should fail when $v is not set"
|
|
elif echo "$out" | grep -q "$v"; then
|
|
ok "$s names missing $v"
|
|
else
|
|
bad "$s failed but did not mention $v"; echo "$out" | sed 's/^/ /'
|
|
fi
|
|
done
|
|
if grep -nE '/dev/sda2|10\.42\.[01]\.44|39118305-4927' failover/sync-*-backup.sh; then
|
|
bad "hardcoded host values remain in the backup scripts"
|
|
else
|
|
ok "no hardcoded devices/UUIDs/addresses in the backup scripts"
|
|
fi
|
|
|
|
# 8. ./deploy.sh --diff runs (no root) and lists rendered files
|
|
if out=$(./deploy.sh --diff hosts/van.conf 2>&1) && echo "$out" | grep -qE '^(NEW|CHANGED) '; then
|
|
ok "deploy.sh --diff hosts/van.conf"
|
|
else
|
|
bad "deploy.sh --diff hosts/van.conf"; echo "$out" | sed 's/^/ /'
|
|
fi
|
|
if echo "$out" | grep -qE '^(NEW|CHANGED) +/etc/systemd/system/van-ap-dnsmasq\.service$'; then
|
|
ok "--diff covers plain-install files"
|
|
else
|
|
bad "--diff does not list plain-install file van-ap-dnsmasq.service"
|
|
fi
|
|
|
|
# 9. no interface name of one host hardcoded in a shipped file (the hostapd-2g.conf class of bug)
|
|
for c in hosts/*.conf; do
|
|
for v in WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE; do
|
|
val=$(unset "$v"; . "./$c"; echo "${!v}")
|
|
if hits=$(git grep -nF -- "$val" -- . ':!hosts' ':!docs' ':!tests' ':!*.md' ':!*.bak'); then
|
|
bad "$c: $v value '$val' is hardcoded outside hosts/"; echo "$hits" | sed 's/^/ /'
|
|
else
|
|
ok "$c: $v value '$val' appears only in hosts/"
|
|
fi
|
|
done
|
|
done
|
|
|
|
# 10. --diff never touches seed-if-absent secret files, and redacts secret values
|
|
out=$(./deploy.sh --diff hosts/wan.conf 2>&1) || true
|
|
leak=$(echo "$out" | grep -E '/etc/van-gps/config\.json|/etc/van-li3/config\.json|/etc/van-battery/pushover\.json') || true
|
|
if [ -z "$leak" ]; then
|
|
ok "--diff ignores seeded secret files"
|
|
else
|
|
bad "--diff mentions seeded secret files"; echo "$leak" | sed 's/^/ /'
|
|
fi
|
|
red=$(printf '%s\n' 'wpa_passphrase=hunter2' '+ "password": "hunter2",' '- "token": "abc123"' ' "user": "u123"' | ./deploy.sh --redact 2>&1) || true
|
|
if echo "$red" | grep -qE 'hunter2|abc123|u123'; then
|
|
bad "--redact leaks a secret value"; echo "$red" | sed 's/^/ /'
|
|
elif echo "$red" | grep -q '^wpa_passphrase=' && echo "$red" | grep -q '^+ "password"' \
|
|
&& echo "$red" | grep -q '^- "token"' && echo "$red" | grep -q '^ "user"'; then
|
|
ok "--redact masks values, keeps keys and diff markers"
|
|
else
|
|
bad "--redact lost keys or diff markers"; echo "$red" | sed 's/^/ /'
|
|
fi
|
|
|
|
# 11. --post-check: no root, warns on this (non-router) machine, exits 1; host-aware; no stale text
|
|
pc=$(POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf 2>&1) && rc=0 || rc=$?
|
|
if [ "$rc" = 1 ] && echo "$pc" | grep -q '^== post-deploy check ==$' && echo "$pc" | grep -q -- '-> post-deploy:'; then
|
|
ok "--post-check hosts/van.conf: heading, warnings, exit 1"
|
|
else
|
|
bad "--post-check hosts/van.conf (exit $rc)"; echo "$pc" | sed 's/^/ /'
|
|
fi
|
|
tmpconf=$(mktemp); trap 'rm -f "$empty" "$tmpconf"' EXIT
|
|
{ grep -v '^ENABLE_NVME_WATCH=' hosts/wan.conf; echo 'ENABLE_NVME_WATCH=0'; } > "$tmpconf"
|
|
pc=$(POSTCHECK_WAIT=0 ./deploy.sh --post-check "$tmpconf" 2>&1) || true
|
|
if ! echo "$pc" | grep -q '^== post-deploy check ==$'; then bad "--post-check on temp conf did not run"; echo "$pc" | sed 's/^/ /'
|
|
elif echo "$pc" | grep -q 'van-nvme-watch'; then bad "--post-check mentions van-nvme-watch with ENABLE_NVME_WATCH=0"
|
|
else ok "--post-check skips van-nvme-watch when ENABLE_NVME_WATCH=0"; fi
|
|
if grep -q 'USB hub' deploy.sh; then bad "stale 'USB hub' text remains in deploy.sh"; else ok "no stale 'USB hub' text in deploy.sh"; fi
|
|
|
|
# 12. unit-state classification used by post_check (run-to-completion units are not failures)
|
|
for row in "active success 0:ok" "inactive success 0:finished" "failed exit-code 1:warn" \
|
|
"activating success 0:wait" "inactive exit-code 1:warn" "inactive success 1:warn"; do
|
|
args=${row%%:*}; want=${row##*:}
|
|
# shellcheck disable=SC2086
|
|
got=$(./deploy.sh --classify-unit $args 2>&1) || true
|
|
if [ "$got" = "$want" ]; then ok "--classify-unit $args -> $want"; else bad "--classify-unit $args gave '$got', want '$want'"; fi
|
|
done
|
|
|
|
# 13. radio beaconing classification used by post_check (present but no ssid = not beaconing)
|
|
r_ok=$'Interface wlan1\n\tifindex 5\n\ttype AP\n\tssid VanLink2\n\tchannel 149 (5745 MHz), width: 80 MHz, center1: 5775 MHz'
|
|
r_bad=$'Interface wlxd8ec5e2faa8c\n\tifindex 6\n\twdev 0x100000001\n\taddr d8:ec:5e:2f:aa:8c\n\ttype AP\n\twiphy 39\n\ttxpower 30.00 dBm'
|
|
for row in "beaconing:$r_ok" "silent:$r_bad" "silent:"; do
|
|
want=${row%%:*}; inp=${row#*:}
|
|
got=$(printf '%s\n' "$inp" | ./deploy.sh --classify-radio 2>&1) || true
|
|
if [ "$got" = "$want" ]; then ok "--classify-radio -> $want"; else bad "--classify-radio gave '$got', want '$want'"; fi
|
|
done
|
|
|
|
# 14. the netplan split leaves the Pi 5's rendered WAN netplan byte-identical, and the R5C (no Wi-Fi WAN) omits wifis
|
|
old=$(mktemp)
|
|
git show 223652b:ap/50-van-wan.yaml > "$old"
|
|
want=$(VANLINK_CONF=hosts/wan.conf ./deploy.sh render "$old")
|
|
tmpw=$(mktemp)
|
|
# render_wan_netplan is internal; expose it through --render-wan-netplan (documented in the header)
|
|
VANLINK_CONF=hosts/wan.conf ./deploy.sh --render-wan-netplan > "$tmpw"
|
|
if [ "$want" = "$(cat "$tmpw")" ]; then ok "wan.conf: split netplan renders identical to the old single template"; else bad "wan.conf: netplan render changed by the split"; diff <(echo "$want") "$tmpw" | head -10 | sed 's/^/ /'; fi
|
|
if VANLINK_CONF=hosts/van.conf ./deploy.sh --render-wan-netplan | grep -q '^ wifis:'; then bad "van.conf: wifis stanza must be omitted (WIFI_WAN_IFACE empty)"; else ok "van.conf: no wifis stanza"; fi
|
|
rm -f "$tmpw" "$old"
|
|
|
|
# 15. cellular connection name comes from the host config
|
|
expect van failover/config.json '"connection": "Koodo"'
|
|
expect wan failover/config.json '"connection": "Koodo"'
|
|
|
|
# 16. cellular keyfile template renders a valid NM gsm profile with a stable uuid
|
|
for h in van wan; do
|
|
k=$(VANLINK_CONF=hosts/$h.conf ./deploy.sh render ap/cellular.nmconnection)
|
|
for pat in '^id=Koodo$' '^type=gsm$' '^apn=sp.koodo.com$' '^route-metric=300$' '^never-default=true$' '^uuid=[0-9a-f-]{36}$'; do
|
|
if echo "$k" | grep -qE "$pat"; then ok "$h: cellular keyfile has $pat"; else bad "$h: cellular keyfile lacks $pat"; fi
|
|
done
|
|
done
|
|
|
|
# 17. --wan-plan describes what would happen (config-derived lines only; no root, no changes)
|
|
for h in van wan; do
|
|
if ! p=$(VANLINK_CONF=hosts/$h.conf ./deploy.sh --wan-plan 2>&1); then bad "$h: --wan-plan failed"; echo "$p" | sed 's/^/ /'; continue; fi
|
|
echo "$p" | grep -q '^wan-plan: ENABLE_NM_WAN=1$' && ok "$h: wan-plan header" || bad "$h: wan-plan header"
|
|
echo "$p" | grep -q 'cellular profile: Koodo apn=sp.koodo.com' && ok "$h: wan-plan cellular" || bad "$h: wan-plan cellular"
|
|
done
|
|
p=$(VANLINK_CONF=hosts/van.conf ./deploy.sh --wan-plan); echo "$p" | grep -q 'wifi WAN: none' && ok "van: wan-plan says no Wi-Fi WAN" || bad "van: wan-plan Wi-Fi WAN"
|
|
p=$(VANLINK_CONF=hosts/wan.conf ./deploy.sh --wan-plan); echo "$p" | grep -q 'wifi WAN: wlan0' && ok "wan: wan-plan Wi-Fi WAN wlan0" || bad "wan: wan-plan Wi-Fi WAN"
|
|
# CELLULAR_UUID is derived in pure bash (no uuidgen dependency) and stable
|
|
u1=$(VANLINK_CONF=hosts/van.conf ./deploy.sh render ap/cellular.nmconnection | grep '^uuid=')
|
|
u2=$(VANLINK_CONF=hosts/wan.conf ./deploy.sh render ap/cellular.nmconnection | grep '^uuid=')
|
|
[ -n "$u1" ] && [ "$u1" != "uuid=" ] && [ "$u1" = "$u2" ] && ok "cellular uuid non-empty and stable across hosts" || bad "cellular uuid: '$u1' vs '$u2'"
|
|
[ "$(grep -v '^[[:space:]]*#' deploy.sh | grep -c uuidgen)" = 0 ] && ok "deploy.sh does not call uuidgen" || bad "deploy.sh calls uuidgen"
|
|
|
|
# 17b. an existing cellular profile is found by id, whatever its file name
|
|
nmd=$(mktemp -d); nme=$(mktemp -d)
|
|
printf '[connection]\nid=Koodo\ntype=gsm\n' > "$nmd/weird-file-name.nmconnection"
|
|
p=$(VANLINK_NM_DIR=$nmd VANLINK_CONF=hosts/van.conf ./deploy.sh --wan-plan 2>&1)
|
|
echo "$p" | grep -q 'cellular profile: Koodo apn=sp.koodo.com exists' && ok "wan-plan: profile found under another file name" || bad "wan-plan: profile under other file name not found"
|
|
if command -v nmcli >/dev/null 2>&1 && nmcli -g NAME connection show 2>/dev/null | grep -Fxq Koodo; then
|
|
ok "wan-plan would-create assertion skipped (this machine's NM lists Koodo)"
|
|
else
|
|
p=$(VANLINK_NM_DIR=$nme VANLINK_CONF=hosts/van.conf ./deploy.sh --wan-plan 2>&1)
|
|
echo "$p" | grep -q 'cellular profile: Koodo apn=sp.koodo.com would create' && ok "wan-plan: empty NM dir -> would create" || bad "wan-plan: empty NM dir should say would create"
|
|
fi
|
|
rm -rf "$nmd" "$nme"
|
|
# 17c. NM must not auto-create "Wired connection" profiles
|
|
[ "$(grep -v '^[[:space:]]*#' ap/nm-no-auto-default.conf 2>/dev/null | grep -v '^[[:space:]]*$')" = "$(printf '[main]\nno-auto-default=*')" ] && ok "nm-no-auto-default.conf is [main] + no-auto-default=*" || bad "ap/nm-no-auto-default.conf missing or wrong"
|
|
|
|
# 18. the detached apply helper: syntax, then behaviour against stubbed system commands in a sandbox
|
|
h=$(VANLINK_CONF=hosts/van.conf ./deploy.sh --print-apply-helper 2>&1) || { bad "--print-apply-helper failed"; echo "$h" | sed 's/^/ /'; h=; }
|
|
if [ -n "$h" ] && [ "$(id -u)" = 0 ]; then
|
|
bad "check 18 refuses to run the helper sandbox as root"
|
|
elif [ -n "$h" ]; then
|
|
echo "$h" | bash -n && ok "apply helper: bash -n" || bad "apply helper: bash -n"
|
|
echo "$h" | grep -q 'enP2p33s0' && ok "apply helper waits on the Starlink port" || bad "apply helper does not reference STARLINK_IFACE"
|
|
echo "$h" | grep -q 'systemctl restart van-failover' && ok "apply helper restarts van-failover" || bad "apply helper lacks van-failover restart"
|
|
stubs=$(mktemp -d)
|
|
for c in netplan nmcli ip ping systemctl reboot; do
|
|
printf '#!/bin/bash\necho "%s $*" >> "$STUB_CALLS"\n' "$c" > "$stubs/$c"
|
|
done
|
|
cat >> "$stubs/netplan" <<'STUB'
|
|
if [ "$1" = apply ]; then
|
|
if [ -e "${STUB_HOLD:-/nonexistent}" ]; then
|
|
: > "$STUB_SB/applying"
|
|
while [ -e "$STUB_HOLD" ]; do /bin/sleep 0.05; done
|
|
fi
|
|
n=$(cat "$STUB_SB/napply" 2>/dev/null || echo 0); echo $((n + 1)) > "$STUB_SB/napply"
|
|
[ "${STUB_FAIL_APPLY:-}" = first ] && [ "$n" = 0 ] && exit 1
|
|
fi
|
|
exit 0
|
|
STUB
|
|
cat >> "$stubs/nmcli" <<'STUB'
|
|
case "$2" in
|
|
GENERAL.STATE) echo "${NM_STATE-100 (connected)}" ;;
|
|
GENERAL.CONNECTION) echo "${NM_CONN-netplan-enP2p33s0}" ;;
|
|
IP4.ADDRESS) echo "${NM_ADDR-192.168.100.10/24}" ;;
|
|
IP4.GATEWAY) echo "${NM_GW-}" ;;
|
|
# never-default: IP4.GATEWAY is empty, the router is only in the DHCP lease options (nmcli -g joins them with " | ")
|
|
DHCP4.OPTION) echo "${NM_DHCP-requested_routers = 1 | routers = 192.168.100.1 | subnet_mask = 255.255.255.0}" ;;
|
|
esac
|
|
exit 0
|
|
STUB
|
|
cat >> "$stubs/ip" <<'STUB'
|
|
if [ "$2" = get ]; then echo "1.1.1.1 via 192.168.100.1 dev x"; exit 0; fi
|
|
case "${STUB_ROUTE:-yes}" in
|
|
yes) echo "default via 192.168.100.1 dev x" ;;
|
|
after-rollback) [ -e "$STUB_ND/50-van-wan.yaml" ] || echo "default via 192.168.100.1 dev x" ;;
|
|
esac
|
|
exit 0
|
|
STUB
|
|
cat >> "$stubs/ping" <<'STUB'
|
|
case " $* " in *" -I "*) exit "${STUB_GW_PING:-0}" ;; esac
|
|
exit "${STUB_NET_PING:-0}"
|
|
STUB
|
|
printf '#!/bin/bash\nexec /bin/sleep 0.05\n' > "$stubs/sleep"
|
|
printf '#!/bin/bash\n[ "$1" = -k ] && shift 2\nshift\nexec "$@"\n' > "$stubs/timeout"
|
|
printf '#!/bin/bash\nexit 0\n' >> "$stubs/systemctl"
|
|
chmod +x "$stubs"/*
|
|
stubs_ok=1
|
|
for c in netplan nmcli ip ping systemctl reboot sleep timeout; do [ -x "$stubs/$c" ] || stubs_ok=0; done
|
|
[ "$stubs_ok" = 1 ] && ok "apply helper sandbox: all system commands stubbed" || bad "apply helper sandbox: stub missing"
|
|
|
|
an_prep() { # fresh sandbox + generated helper; sets sb
|
|
sb=$(mktemp -d); mkdir -p "$sb/netplan" "$sb/sysnet"
|
|
printf 'armbian\n' > "$sb/netplan/10-dhcp-all-interfaces.yaml"; chmod 0600 "$sb/netplan/10-dhcp-all-interfaces.yaml"
|
|
printf 'old\n' > "$sb/netplan/60-other.yaml"
|
|
cp -a "$sb/netplan" "$sb/bak"
|
|
[ -n "${AN_NOPORT:-}" ] || mkdir "$sb/sysnet/enP2p33s0"
|
|
[ -z "${AN_NOBAK:-}" ] || rm -rf "$sb/bak"
|
|
[ -z "${AN_BAKBAD:-}" ] || chmod 000 "$sb/bak/60-other.yaml"
|
|
printf 'network:\n version: 2\n' > "$sb/rendered.yaml"
|
|
VANLINK_CONF=hosts/van.conf ./deploy.sh --print-apply-helper --backup "$sb/bak" --netplan-dir "$sb/netplan" \
|
|
--log "$sb/log" --status "$sb/status" --sysnet "$sb/sysnet" --rendered "$sb/rendered.yaml" \
|
|
--deadlines "1 1 1" --stamp T > "$sb/helper.sh"
|
|
# never run a helper that does not point at the sandbox
|
|
grep -qF "nd='$sb/netplan'" "$sb/helper.sh" && [ "$(id -u)" != 0 ] || { bad "helper does not bake the sandbox netplan dir (or root) - refusing to run"; return 1; }
|
|
}
|
|
an_run() { # an_run [VAR=val ...] — fresh sandbox, run the helper; sets sb rc
|
|
an_prep || { rc=99; return 0; }
|
|
env PATH="$stubs:/usr/bin:/bin" STUB_CALLS="$sb/calls" STUB_SB="$sb" STUB_ND="$sb/netplan" "$@" bash "$sb/helper.sh" && rc=0 || rc=$?
|
|
}
|
|
a_is() { [ "$2" = "$3" ] && ok "apply helper $1" || bad "apply helper $1 (got '$2', want '$3')"; }
|
|
st_word() { cut -d' ' -f1 "$sb/status" 2>/dev/null; }
|
|
# (1) success
|
|
an_run
|
|
a_is "success: exit 0" "$rc" 0; a_is "success: status" "$(st_word)" ok
|
|
grep -q '^ping -c1 -W2 -I enP2p33s0 192.168.100.1$' "$sb/calls" && ok "apply helper success: gateway from the DHCP routers option" || bad "apply helper success: routers option not used"
|
|
[ ! -e "$sb/helper.sh" ] && ok "apply helper success: helper removed itself" || bad "apply helper success: helper file left behind"
|
|
grep -q 'route to 1.1.1.1:' "$sb/log" && ok "apply helper success: route logged" || bad "apply helper success: no route log"
|
|
grep -q 'apply-network OK' "$sb/log" && ok "apply helper success: log OK" || bad "apply helper success: log"
|
|
a_is "success: 50-van-wan.yaml mode" "$(stat -c %a "$sb/netplan/50-van-wan.yaml" 2>/dev/null)" 600
|
|
[ -e "$sb/netplan/10-dhcp-all-interfaces.yaml.vanlink-disabled" ] && [ ! -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && ok "apply helper success: Armbian file moved aside" || bad "apply helper success: Armbian file not moved aside"
|
|
grep -q '^systemctl restart van-failover' "$sb/calls" && ok "apply helper success: van-failover restarted" || bad "apply helper success: no van-failover restart"
|
|
rm -rf "$sb"
|
|
# (2) stage 1 fails: connected (externally)
|
|
an_run NM_STATE="100 (connected (externally))"
|
|
a_is "stage1: exit 1" "$rc" 1; a_is "stage1: status" "$(st_word)" rolled-back
|
|
grep -q 'ROLLED BACK (restored, connectivity ok)' "$sb/log" && ok "apply helper stage1: log ROLLED BACK" || bad "apply helper stage1: log"
|
|
a_is "stage1: Armbian file restored mode" "$(stat -c %a "$sb/netplan/10-dhcp-all-interfaces.yaml" 2>/dev/null)" 600
|
|
[ ! -e "$sb/netplan/50-van-wan.yaml" ] && [ ! -e "$sb/netplan/10-dhcp-all-interfaces.yaml.vanlink-disabled" ] && [ -e "$sb/netplan/60-other.yaml" ] && ok "apply helper stage1: netplan dir restored wholesale" || bad "apply helper stage1: netplan dir not restored"
|
|
rm -rf "$sb"
|
|
# (2b) stage 1 fails: wrong connection name; gateway unpingable
|
|
an_run NM_CONN="Wired connection 1"; a_is "stage1 conn name: status" "$(st_word)" rolled-back; rm -rf "$sb"
|
|
an_run STUB_GW_PING=1; a_is "stage1 gateway ping: status" "$(st_word)" rolled-back; rm -rf "$sb"
|
|
an_run NM_DHCP="requested_routers = 1 | subnet_mask = 255.255.255.0"; a_is "stage1 no IP4.GATEWAY and no routers option: status" "$(st_word)" rolled-back; rm -rf "$sb"
|
|
an_run NM_DHCP="routers = 192.168.100.1 192.168.100.2"; a_is "routers option with two routers: status" "$(st_word)" ok
|
|
grep -q '^ping -c1 -W2 -I enP2p33s0 192.168.100.1$' "$sb/calls" && ok "apply helper: pinged the first router" || bad "apply helper: first router not pinged"; rm -rf "$sb"
|
|
an_run NM_GW=192.168.100.9 NM_DHCP=; a_is "IP4.GATEWAY present: status" "$(st_word)" ok
|
|
grep -q '^ping -c1 -W2 -I enP2p33s0 192.168.100.9$' "$sb/calls" && ok "apply helper: pinged IP4.GATEWAY" || bad "apply helper: IP4.GATEWAY not pinged"; rm -rf "$sb"
|
|
# (3) netplan apply fails, port absent
|
|
AN_NOPORT=1 an_run STUB_FAIL_APPLY=first
|
|
a_is "apply failure: exit 1" "$rc" 1; a_is "apply failure: not ok" "$(st_word)" rolled-back
|
|
[ -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && [ ! -e "$sb/netplan/50-van-wan.yaml" ] && ok "apply helper apply failure: restored" || bad "apply helper apply failure: not restored"
|
|
rm -rf "$sb"
|
|
# (3b) port absent and apply ok: stage 1 needs only the apply
|
|
AN_NOPORT=1 an_run; a_is "port absent, apply ok: status" "$(st_word)" ok; rm -rf "$sb"
|
|
# (4) backup dir missing: undo own changes, report failure, never reboot into the new config
|
|
AN_NOBAK=1 an_run NM_STATE=bad STUB_ROUTE=none
|
|
a_is "backup missing: exit 1" "$rc" 1; a_is "backup missing: status" "$(st_word)" rollback-failed
|
|
grep -q 'ROLLBACK FAILED: BACKUP-MISSING' "$sb/log" && ok "apply helper backup missing: loud log" || bad "apply helper backup missing: log"
|
|
grep -q '^systemctl reboot' "$sb/calls" && bad "apply helper backup missing: must not reboot" || ok "apply helper backup missing: no reboot"
|
|
[ ! -e "$sb/netplan/50-van-wan.yaml" ] && [ -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && [ ! -e "$sb/netplan/10-dhcp-all-interfaces.yaml.vanlink-disabled" ] && ok "apply helper backup missing: own changes undone" || bad "apply helper backup missing: own changes not undone"
|
|
rm -rf "$sb"
|
|
# (4b) rollback cp fails (unreadable backup file) but connectivity is fine: loud, no reboot
|
|
AN_BAKBAD=1 an_run NM_STATE=bad
|
|
a_is "cp failure: status" "$(st_word)" rollback-failed
|
|
grep -q 'ROLLBACK FAILED: CP-FAILED' "$sb/log" && ok "apply helper cp failure: loud log" || bad "apply helper cp failure: log"
|
|
grep -q '^systemctl reboot' "$sb/calls" && bad "apply helper cp failure with connectivity: must not reboot" || ok "apply helper cp failure with connectivity: no reboot"
|
|
chmod 600 "$sb/bak/60-other.yaml"; rm -rf "$sb"
|
|
# (5) stage 2 fails: no default route until rolled back
|
|
an_run STUB_ROUTE=after-rollback
|
|
a_is "stage2: exit 1" "$rc" 1; a_is "stage2: status" "$(st_word)" rolled-back
|
|
[ ! -e "$sb/netplan/50-van-wan.yaml" ] && [ -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && ok "apply helper stage2: restored" || bad "apply helper stage2: not restored"
|
|
rm -rf "$sb"
|
|
# (6) clean rollback but no connectivity (uplink unplugged): must NOT reboot
|
|
an_run STUB_ROUTE=none
|
|
a_is "no connectivity after clean rollback: exit" "$rc" 1
|
|
a_is "no connectivity after clean rollback: status" "$(cut -d' ' -f1,3 "$sb/status" 2>/dev/null)" "rolled-back restored-no-connectivity;"
|
|
grep -q 'restored-no-connectivity; stage 2' "$sb/status" && ok "apply helper: original reason kept in status" || bad "apply helper: reason lost from status"
|
|
grep -q 'restored, but no connectivity' "$sb/log" && ok "apply helper: no-connectivity log" || bad "apply helper: no-connectivity log"
|
|
grep -q '^systemctl reboot' "$sb/calls" && bad "apply helper: must not reboot after a clean rollback" || ok "apply helper: no reboot after a clean rollback"
|
|
[ -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && [ ! -e "$sb/netplan/50-van-wan.yaml" ] && ok "apply helper: clean rollback restored files" || bad "apply helper: clean rollback files"
|
|
rm -rf "$sb"
|
|
# (7) rollback cp fails AND no connectivity: last-resort reboot
|
|
AN_BAKBAD=1 an_run STUB_ROUTE=none
|
|
a_is "failed rollback, no connectivity: status" "$(st_word)" rollback-failed
|
|
grep -q '^systemctl reboot' "$sb/calls" && ok "apply helper: reboots when the rollback failed and connectivity is absent" || bad "apply helper: no reboot after failed rollback"
|
|
rm -rf "$sb"
|
|
# (8) TERM while a slow netplan apply is running: the trap restores, once
|
|
an_prep && {
|
|
: > "$sb/hold"
|
|
env PATH="$stubs:/usr/bin:/bin" STUB_CALLS="$sb/calls" STUB_SB="$sb" STUB_ND="$sb/netplan" STUB_HOLD="$sb/hold" bash "$sb/helper.sh" &
|
|
hp=$!
|
|
for _ in $(seq 1 100); do [ -e "$sb/applying" ] && break; /bin/sleep 0.05; done
|
|
[ -e "$sb/applying" ] && ok "apply helper TERM: helper reached the slow apply" || bad "apply helper TERM: never reached the apply"
|
|
a_is "TERM: status while running" "$(cut -d' ' -f1,3 "$sb/status" 2>/dev/null)" "running $sb/bak"
|
|
kill -TERM "$hp"; rm -f "$sb/hold"
|
|
wait "$hp" && rc=0 || rc=$?
|
|
a_is "TERM: exit 1" "$rc" 1
|
|
a_is "TERM: status" "$(cut -d' ' -f1,3 "$sb/status" 2>/dev/null)" "rolled-back terminated"
|
|
[ -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && [ ! -e "$sb/netplan/50-van-wan.yaml" ] && ok "apply helper TERM: restored" || bad "apply helper TERM: not restored"
|
|
[ "$(grep -c 'restoring' "$sb/log")" = 1 ] && ok "apply helper TERM: restore ran once" || bad "apply helper TERM: restore count"
|
|
rm -rf "$sb"
|
|
}
|
|
rm -rf "$stubs"
|
|
# 18a. ordering that a behavioural test cannot reach (a signal at an exact instant), checked on the text
|
|
r1=$(echo "$h" | grep -A2 '^restore() {' | sed -n 2p)
|
|
[ "$r1" = " trap '' TERM INT HUP" ] && ok "apply helper: restore() ignores signals before its guard" || bad "apply helper: restore() must start with trap '' (got '$r1')"
|
|
so=$(echo "$h" | grep -n '^status ok$' | cut -d: -f1); tr=$(echo "$h" | grep -n '^trap - TERM INT HUP$' | cut -d: -f1)
|
|
[ -n "$so" ] && [ -n "$tr" ] && [ "$so" -lt "$tr" ] && ok "apply helper: status ok is written before the trap is removed" || bad "apply helper: status ok must precede trap removal"
|
|
echo "$h" | grep -q "^trap 'rm -f \"\$0\"' EXIT$" && ok "apply helper removes itself on exit" || bad "apply helper lacks the self-remove EXIT trap"
|
|
echo "$h" | grep -q 'timeout -k 10 30 netplan generate' && echo "$h" | grep -q 'timeout -k 10 90 netplan apply' && ok "apply helper: netplan under timeout -k" || bad "apply helper: netplan lacks timeout -k"
|
|
fi
|
|
# 18b. status file gate and STARLINK_IFACE validation
|
|
gs=$(mktemp -d)
|
|
for w in running rollback-failed ok rolled-back; do
|
|
echo "$w 2026-01-01T00:00:00+00:00 x" > "$gs/s"
|
|
case $w in running|rollback-failed) want=yes ;; *) want=no ;; esac
|
|
[ "$(./deploy.sh --apply-status-check "$gs/s")" = "$want" ] && ok "apply-status-check: $w -> $want" || bad "apply-status-check: $w should be $want"
|
|
done
|
|
[ "$(./deploy.sh --apply-status-check "$gs/none")" = no ] && ok "apply-status-check: no file -> no" || bad "apply-status-check: no file"
|
|
sed 's/^STARLINK_IFACE=.*/STARLINK_IFACE="a'"'"'b"/' hosts/van.conf > "$gs/bad.conf"
|
|
if ./deploy.sh --check "$gs/bad.conf" >/dev/null 2>&1; then bad "STARLINK_IFACE with a quote must be rejected"; else ok "STARLINK_IFACE with a quote is rejected"; fi
|
|
rm -rf "$gs"
|
|
|
|
exit "$fail"
|