Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
197 lines
9.8 KiB
Bash
Executable File
197 lines
9.8 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)
|
|
exempt=" cockpit/vanrouter/vanrouter.js ap/50-van-wan.yaml "
|
|
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": "wlan0"'
|
|
expect van router-status/config.json '"wifi_wan_iface": "wlan0"'
|
|
expect van 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
|
|
|
|
exit "$fail"
|