diff --git a/ap/nm-uplink-pending.conf b/ap/nm-uplink-pending.conf new file mode 100644 index 0000000..e75de96 --- /dev/null +++ b/ap/nm-uplink-pending.conf @@ -0,0 +1,9 @@ +# Installed by ./deploy.sh as /etc/NetworkManager/conf.d/zz-van-wan-pending.conf only while the +# WAN netplan (/etc/netplan/50-van-wan.yaml) is missing, i.e. before the uplink port has been +# handed to NetworkManager: until then systemd-networkd owns the port (DHCP lease, default route) +# and NM must leave it alone, or it shows up as "connected (externally)" and van-failover starts +# steering it. ./deploy.sh --apply-network removes this file right before it applies the netplan +# (and a rollback puts it back); a routine deploy removes it once the netplan matches. +# "+=" appends to the unmanaged list of van-ap-unmanaged.conf; the zz- name sorts after it. +[keyfile] +unmanaged-devices+=interface-name:@STARLINK_IFACE@ diff --git a/deploy.sh b/deploy.sh index 43e0ccb..0a54324 100755 --- a/deploy.sh +++ b/deploy.sh @@ -14,16 +14,23 @@ # ./deploy.sh --apply-network [--force] # root: a detached helper installs the WAN netplan, applies it, gates on uplink health (2 stages) and rolls back # # automatically on failure; status in /var/lib/vanlink/apply-network.status. No-op when already applied, and refused after a # # running/rollback-failed status, unless --force. +# # Refused (unless --force) when the preflight finds a problem, or when an existing +# # /etc/netplan/50-van-wan.yaml differs from the rendered one (the diff is shown, secrets redacted). +# ./deploy.sh --apply-preflight [conf] # problems --apply-network would refuse on, one per line (none: no output, exit 0): +# # STARLINK_IFACE absent in sysfs; /etc/vanlink.conf -> hosts/.conf with != `hostname -s` # ./deploy.sh --apply-status-check # test seam: yes|no, would this status file (running|rollback-failed) block --apply-network # ./deploy.sh --print-apply-helper [conf] # print the detached apply/rollback helper script (read-only, no root; for review and tests) -# # test-only flags: --backup --netplan-dir --log --status --sysnet --rendered --deadlines "s1 s2 rc" --stamp +# # test-only flags: --backup --netplan-dir --log --status --sysnet --rendered --nm-conf-dir --deadlines "s1 s2 rc" --stamp # ./deploy.sh render # preview a templated file on stdout # ./deploy.sh --redact # filter stdin, masking secret values (used by --diff; for tests) -# VANLINK_SYSNET= # test override: sysfs net dir used by --post-check (default /sys/class/net) +# VANLINK_CONF= # host config path (default /etc/vanlink.conf); sub-commands taking [conf] use that argument instead +# VANLINK_SYSNET= # test override: sysfs net dir used by --post-check and --apply-preflight (default /sys/class/net) +# VANLINK_HOSTNAME= # test override: host name used by --apply-preflight (default `hostname -s`; ignored when run as root) # VANLINK_FAILOVER_STATE= # test override: van-failover state file read by --post-check (default /run/van-failover/state.json) # VANLINK_APPLY_STATUS= # test override: apply-network status file read by --post-check (default /var/lib/vanlink/apply-network.status) # VANLINK_WAN_NETPLAN= # test override: installed WAN netplan compared by nm_wan_state_netplan (default /etc/netplan/50-van-wan.yaml) -# VANLINK_NM_DIR= # test override: where existing NM *.nmconnection files are looked up (default /etc/NetworkManager/system-connections) +# VANLINK_NM_DIR= # test override: where existing NM *.nmconnection files are looked up (default /etc/NetworkManager/system-connections; +# # ignored when run as root) # Idempotent. Netplan (the NM-managed WANs; eth0 is a LAN port on br0) is never # changed by a routine deploy (applying it flaps the uplinks). ENABLE_NM_WAN=0: # applied manually from the reference copy in ap/50-van-wan.yaml, and checked for @@ -106,15 +113,37 @@ render_wan_netplan() { # ap/50-van-wan.yaml, plus the wifis stanza when the hos fi } -nm_wan_state_netplan() { # missing | differs | matches (the installed WAN netplan vs the rendered one) +nm_wan_state_netplan() { # missing | unreadable | differs | matches (the installed WAN netplan vs the rendered one) local f=${VANLINK_WAN_NETPLAN:-/etc/netplan/50-van-wan.yaml} if [ ! -f "$f" ]; then echo missing + elif [ ! -r "$f" ]; then echo unreadable # installed 0600 by --apply-network; only root can compare it elif diff -q <(render_wan_netplan) "$f" >/dev/null 2>&1; then echo matches else echo differs; fi } +nm_pkg_installed() { # true if the network-manager package is installed (dpkg's Status, not just known to dpkg) + local s + s=$(dpkg-query -W -f='${Status}' network-manager 2>/dev/null) || return 1 + [ "$s" = "install ok installed" ] +} + +# Keeps NetworkManager off the uplink port until --apply-network hands it over (ap/nm-uplink-pending.conf). +NM_PENDING_CONF=/etc/NetworkManager/conf.d/zz-van-wan-pending.conf +# nm_pending_action -> would install | installed | would remove | absent +# The routine deploy installs the pending conf only while the WAN netplan is missing (the uplink was never +# handed to NM) and removes it once it matches. differs/unreadable: the port was already handed to NM by an +# earlier netplan, so the file is left as it is (installing it would take a live uplink away from NM). +nm_pending_action() { + case "$1" in + missing) if [ -e "$2" ]; then echo installed; else echo "would install"; fi ;; + matches) if [ -e "$2" ]; then echo "would remove"; else echo absent; fi ;; + *) if [ -e "$2" ]; then echo installed; else echo absent; fi ;; + esac +} + cellular_profile_exists() { # true if an NM profile with id=$CELLULAR_CONN_NAME exists (any file name, or known to nmcli) - local d=${VANLINK_NM_DIR:-/etc/NetworkManager/system-connections} f names + local d=/etc/NetworkManager/system-connections f names + [ "$(id -u)" = 0 ] || d=${VANLINK_NM_DIR:-$d} # the test knob is ignored when run as root for f in "$d"/*.nmconnection; do [ -f "$f" ] || continue if grep -Fx "id=${CELLULAR_CONN_NAME}" "$f" >/dev/null 2>&1; then return 0; fi @@ -146,7 +175,20 @@ install_rendered() { # install_rendered [mode] redact_secrets() { # stdin -> stdout; mask values of secret-looking keys, keep key + diff markers sed -E \ -e 's/((pass(word|phrase)|secret|token)[A-Za-z_]*[[:space:]]*=).*/\1/I' \ - -e 's/("[A-Za-z_]*(pass(word|phrase)|secret|token|user)[A-Za-z_]*"[[:space:]]*:[[:space:]]*)"[^"]*"/\1""/I' + -e 's/("[A-Za-z_]*(pass(word|phrase)|secret|token|user)[A-Za-z_]*"[[:space:]]*:[[:space:]]*)"[^"]*"/\1""/I' \ + -e 's/^([-+ ]?[[:space:]]*(pass(word|phrase)|secret|token)[A-Za-z_]*:[[:space:]]*)[^[:space:]].*/\1/I' +} + +# apply_preflight_problems — one line per reason --apply-network should not run +# here (nothing when fine): the uplink port does not exist, or the conf is a hosts/.conf of another host +# (checked only when the conf resolves into a hosts/ directory). +apply_preflight_problems() { + local conf=$1 sn=$2 hn=$3 real + [ -e "$sn/$STARLINK_IFACE" ] || echo "uplink port STARLINK_IFACE=$STARLINK_IFACE does not exist ($sn/$STARLINK_IFACE)" + real=$(readlink -f "$conf") + if [ "$(basename "$(dirname "$real")")" = hosts ] && [ "$(basename "$real" .conf)" != "$hn" ]; then + echo "$conf -> $real is not this host's config (hostname $hn)" + fi } template_files() { # every repo file that carries an @TOKEN@ placeholder @@ -336,7 +378,11 @@ PY else warn "post-deploy: van-failover state $fst missing or unreadable"; fi # k. netplan drift f=$(nm_wan_state_netplan) - [ "$f" = matches ] && echo " ok WAN netplan matches" || warn "post-deploy: WAN netplan $f — run ./deploy.sh --apply-network" + case "$f" in + matches) echo " ok WAN netplan matches" ;; + unreadable) echo " ok WAN netplan: cannot read as non-root (run with sudo to verify)" ;; + *) warn "post-deploy: WAN netplan $f — run ./deploy.sh --apply-network" ;; + esac # l. last --apply-network as_f=${VANLINK_APPLY_STATUS:-/var/lib/vanlink/apply-network.status} if [ -r "$as_f" ]; then @@ -353,8 +399,9 @@ PY # --apply-network time budgets (seconds), in one place; all derived from the AN_* constants. # (netplan generate/apply run under timeout -k AN_KILL_T: +AN_KILL_T each in the worst case) -# forward path AN_FWD = generate + apply + 2*kill + stage 1 + restart + stage 2 -# rollback path AN_RB = generate + apply + 2*kill + restart + connectivity recheck +# forward path AN_FWD = NM conf reload + generate + apply + 2*kill + van-br0-setup (unit check + restart) +# + stage 1 + restart + stage 2 + networkd info line + diagnostics (logged once, on a failed gate) +# rollback path AN_RB = generate + apply + 2*kill + NM conf reload + restart + connectivity recheck # helper worst case AN_HELPER_MAX = AN_FWD + AN_RB + AN_SLOP (loop iterations overshoot their # deadline by a few nmcli/ping timeouts) # foreground waits AN_WAIT = AN_HELPER_MAX + 30 @@ -364,8 +411,9 @@ PY # route in its first loop, so a healthy default route shows up within seconds of a restart; the # 120 s stage 2 deadline is a wide margin for a slow Starlink/cellular probe. AN_KILL_T=10 AN_APPLY_T=90 AN_GEN_T=30 AN_NM_T=5 AN_RESTART_T=30 AN_S1=60 AN_S2=120 AN_RC=60 AN_SLOP=60 -AN_FWD=$(( AN_GEN_T + AN_APPLY_T + 2 * AN_KILL_T + AN_S1 + AN_RESTART_T + AN_S2 )) -AN_RB=$(( AN_GEN_T + AN_APPLY_T + 2 * AN_KILL_T + AN_RESTART_T + AN_RC )) +AN_DIAG=$(( 5 * AN_NM_T )) # diag(): five commands, each under timeout AN_NM_T +AN_FWD=$(( AN_NM_T + AN_GEN_T + AN_APPLY_T + 2 * AN_KILL_T + AN_NM_T + AN_RESTART_T + AN_S1 + AN_RESTART_T + AN_S2 + AN_NM_T + AN_DIAG )) +AN_RB=$(( AN_GEN_T + AN_APPLY_T + 2 * AN_KILL_T + AN_NM_T + AN_RESTART_T + AN_RC )) AN_HELPER_MAX=$(( AN_FWD + AN_RB + AN_SLOP )) AN_WAIT=$(( AN_HELPER_MAX + 30 )) AN_RUNMAX=$(( AN_HELPER_MAX + 60 )) @@ -378,36 +426,61 @@ apply_status_blocks_run() { # apply_status_blocks_run -> yes|no: esac } -# apply_helper_script [netplan-dir] [log] [status-file] [sysnet-dir] [rendered-file] [deadlines "s1 s2 rc"] [run-stamp] +# apply_helper_script [netplan-dir] [log] [status-file] [sysnet-dir] [rendered-file] [deadlines "s1 s2 rc"] [run-stamp] [nm-conf-dir] # The script the detached unit runs. Paths are parameters only so tests can bake # sandbox paths into the text; the production helper reads no environment. apply_helper_script() { local bak=$1 nd=${2:-/etc/netplan} log=${3:-/var/log/vanlink-apply-network.log} local st=${4:-/var/lib/vanlink/apply-network.status} sn=${5:-/sys/class/net} local rf=${6:-/run/vanlink-50-van-wan.yaml} dl=${7:-"$AN_S1 $AN_S2 $AN_RC"} stamp=${8:-manual} + local ncd=${9:-$(dirname "$NM_PENDING_CONF")} pc=${NM_PENDING_CONF##*/} local d1 d2 d3 read -r d1 d2 d3 <<< "$dl" # Unquoted heredoc on purpose: values expand now; \$... stays for run time. cat <>"\$log" 2>&1 trap 'rm -f "\$0"' EXIT status() { mkdir -p "\$(dirname "\$st")"; echo "\$1 \$(date -Is)\${2:+ \$2}" > "\$st"; } echo "== apply-network $stamp \$(date -Is) (backup: \$bak)" conn_ok() { ip route show default | grep . >/dev/null && ping -c1 -W2 1.1.1.1 >/dev/null 2>&1; } +gate2() { + conn_ok || return 1 + [ -e "\$sn/$STARLINK_IFACE" ] || return 0 + ip route show default dev '$STARLINK_IFACE' proto static | grep . >/dev/null +} +diag() { # diag : uplink state for the log; informational, every command bounded and failure-tolerant + echo "--- diagnostics: \$1" + echo "--- nmcli device show $STARLINK_IFACE" + timeout $AN_NM_T nmcli -g GENERAL.STATE,GENERAL.CONNECTION,IP4.ADDRESS,IP4.GATEWAY device show '$STARLINK_IFACE' 2>&1 + echo "--- DHCP4 routers" + timeout $AN_NM_T nmcli -g DHCP4.OPTION device show '$STARLINK_IFACE' 2>&1 | tr '|' '\\n' | grep -E '^ *routers =' + echo "--- networkctl status $STARLINK_IFACE" + timeout $AN_NM_T networkctl --no-pager -n 0 status '$STARLINK_IFACE' 2>&1 | head -8 + echo "--- ip -4 addr show $STARLINK_IFACE" + timeout $AN_NM_T ip -4 addr show '$STARLINK_IFACE' 2>&1 + echo "--- ip route" + timeout $AN_NM_T ip route 2>&1 + echo "--- end diagnostics" + return 0 +} gate1() { [ -e "\$sn/$STARLINK_IFACE" ] || return 0 [ "\$(timeout $AN_NM_T nmcli -g GENERAL.STATE device show '$STARLINK_IFACE' 2>/dev/null)" = "100 (connected)" ] || return 1 @@ -433,6 +506,9 @@ restore() { # restore : put the pre-run netplan back, re-check connecti if [ -d "\$bak" ]; then rm -f "\$nd"/* "\$nd"/.[!.]* cp -a "\$bak"/. "\$nd"/ || rb=CP-FAILED + rm -f "\$nd/nm-pending.conf" # the backed-up NM conf is not a netplan file + # the NM conf that kept NM off the uplink goes back too; NM re-reads it after the re-apply below + [ ! -f "\$bak/nm-pending.conf" ] || cp -a "\$bak/nm-pending.conf" "\$ncd/$pc" || echo "WARNING: could not restore \$ncd/$pc" else # No backup to restore from: at least undo this helper's own two changes. rb=BACKUP-MISSING @@ -440,6 +516,7 @@ restore() { # restore : put the pre-run netplan back, re-check connecti [ ! -f "\$nd/10-dhcp-all-interfaces.yaml.vanlink-disabled" ] || mv "\$nd/10-dhcp-all-interfaces.yaml.vanlink-disabled" "\$nd/10-dhcp-all-interfaces.yaml" fi timeout -k $AN_KILL_T $AN_GEN_T netplan generate && timeout -k $AN_KILL_T $AN_APPLY_T netplan apply || { [ "\$rb" != ok ] || rb=APPLY-FAILED; } + [ ! -f "\$ncd/$pc" ] || timeout $AN_NM_T nmcli general reload conf || echo "nmcli general reload conf failed" timeout $AN_RESTART_T systemctl restart van-failover || true end=\$((SECONDS+$d3)) while :; do @@ -464,11 +541,18 @@ restore() { # restore : put the pre-run netplan back, re-check connecti fi exit 1 } +pending_off() { # hand the uplink to NM: drop the NM conf that kept NM off it, right before the apply + [ -e "\$ncd/$pc" ] || return 0 + rm -f "\$ncd/$pc" || return 1 + timeout $AN_NM_T nmcli general reload conf || echo "nmcli general reload conf failed (continuing; stage 1 decides)" + return 0 +} status running "\$bak" trap 'restore terminated' TERM INT HUP applied=0 if install -m0600 "\$rf" "\$nd/50-van-wan.yaml" \\ && { [ ! -f "\$nd/10-dhcp-all-interfaces.yaml" ] || mv "\$nd/10-dhcp-all-interfaces.yaml" "\$nd/10-dhcp-all-interfaces.yaml.vanlink-disabled"; } \\ + && pending_off \\ && timeout -k $AN_KILL_T $AN_GEN_T netplan generate && timeout -k $AN_KILL_T $AN_APPLY_T netplan apply; then applied=1 else @@ -476,6 +560,10 @@ else fi s1=0 if [ "\$applied" = 1 ]; then + # netplan apply restarts networkd, which can drop br0's members: re-run the bridge setup (not gating) + if timeout $AN_NM_T systemctl cat van-br0-setup >/dev/null 2>&1; then + timeout $AN_RESTART_T systemctl restart van-br0-setup || echo "van-br0-setup restart failed (continuing)" + fi end=\$((SECONDS+$d1)) while :; do gate1 && { s1=1; break; } @@ -483,17 +571,19 @@ if [ "\$applied" = 1 ]; then sleep 1 done fi -[ "\$s1" = 1 ] || restore "stage 1: apply or uplink gate failed" +[ "\$s1" = 1 ] || { diag "stage 1"; restore "stage 1: apply or uplink gate failed"; } timeout $AN_RESTART_T systemctl restart van-failover || restore "van-failover restart failed" s2=0 end=\$((SECONDS+$d2)) while :; do - conn_ok && { s2=1; break; } + gate2 && { s2=1; break; } [ "\$SECONDS" -ge "\$end" ] && break sleep 1 done route_log -[ "\$s2" = 1 ] || restore "stage 2: no default route or internet" +[ "\$s2" = 1 ] || { diag "stage 2"; restore "stage 2: no default route, no van-failover route on the port, or no internet"; } +nwl=\$(timeout $AN_NM_T networkctl --no-pager --no-legend list 2>/dev/null | awk -v i='$STARLINK_IFACE' '\$2 == i { print; exit }') +echo "networkd (info, not gating): \${nwl:-$STARLINK_IFACE not listed}" rm -f "\$rf" status ok trap - TERM INT HUP @@ -507,8 +597,7 @@ apply_network() { # ./deploy.sh --apply-network [--force] — explicit, root; c [ "${1:-}" = --force ] && force=1 [ "$(id -u)" = 0 ] || die "--apply-network needs root (sudo)" [ "$ENABLE_NM_WAN" = 1 ] || die "ENABLE_NM_WAN is 0 in $VANLINK_CONF — nothing to apply" - dpkg-query -W -f='${Status}' network-manager 2>/dev/null | grep -q 'install ok installed' \ - || die "network-manager is not installed — run ./deploy.sh first" + nm_pkg_installed || die "network-manager is not installed — run ./deploy.sh first" systemctl is-active --quiet NetworkManager || die "NetworkManager is not running — handing it the uplink port would flap it" systemctl cat van-failover >/dev/null 2>&1 || die "van-failover.service does not exist — run ./deploy.sh first (stage 2 of the gate needs it)" systemctl is-active --quiet vanlink-apply-network && die "an apply is already running — see /var/log/vanlink-apply-network.log" @@ -516,10 +605,23 @@ apply_network() { # ./deploy.sh --apply-network [--force] — explicit, root; c if [ "$force" = 0 ] && [ "$(apply_status_blocks_run "$st")" = yes ]; then die "the last apply did not finish cleanly: $(cat "$st" 2>/dev/null) — inspect $st and $log, check the backup dir named there, then re-run with --force" fi - if [ "$force" = 0 ] && [ "$(VANLINK_WAN_NETPLAN='' nm_wan_state_netplan)" = matches ] && [ ! -e /etc/netplan/10-dhcp-all-interfaces.yaml ]; then + local np_state problems + np_state=$(VANLINK_WAN_NETPLAN='' nm_wan_state_netplan) + if [ "$force" = 0 ] && [ "$np_state" = matches ] && [ ! -e /etc/netplan/10-dhcp-all-interfaces.yaml ]; then echo "nothing to apply (WAN netplan already matches; use --force to re-apply)" return 0 fi + problems=$(apply_preflight_problems "$VANLINK_CONF" /sys/class/net "$(hostname -s)") + if [ -n "$problems" ] && [ "$force" = 0 ]; then + die "refusing to apply: +$problems +(fix it, or re-run with --force)" + fi + if [ "$force" = 0 ] && { [ "$np_state" = differs ] || [ "$np_state" = unreadable ]; }; then + echo "/etc/netplan/50-van-wan.yaml exists and differs from the rendered template (secrets redacted):" + diff -u /etc/netplan/50-van-wan.yaml <(render_wan_netplan) | redact_secrets | sed 's/^/ /' || true + die "refusing to replace an existing, different WAN netplan without --force (review the diff above)" + fi local stamp helper rendered bak pt stamp=$(date +%Y%m%d-%H%M%S)-$$ rendered=/run/vanlink-50-van-wan-$stamp.yaml @@ -528,6 +630,10 @@ apply_network() { # ./deploy.sh --apply-network [--force] — explicit, root; c check_rendered ap/50-van-wan.yaml "$rendered" || { rm -f "$rendered"; exit 1; } bak=$(mktemp -d "/var/backups/vanlink-netplan-$stamp.XXXX") || { rm -f "$rendered"; die "cannot create backup dir"; } cp -a /etc/netplan/. "$bak/" || { rm -f "$rendered"; die "backup failed — nothing changed"; } + # the NM conf keeping NM off the uplink: the helper removes it right before the apply, a rollback restores it from here + if [ -e "$NM_PENDING_CONF" ]; then + cp -a "$NM_PENDING_CONF" "$bak/nm-pending.conf" || { rm -f "$rendered"; die "backup of $NM_PENDING_CONF failed — nothing changed"; } + fi if [ ! -e /var/backups/vanlink-netplan-orig ] && [ ! -e /etc/netplan/50-van-wan.yaml ]; then pt=/var/backups/vanlink-netplan-orig.tmp.$$ rm -rf "$pt" @@ -537,7 +643,7 @@ apply_network() { # ./deploy.sh --apply-network [--force] — explicit, root; c fi install -d -m0755 /var/lib/vanlink rm -f "$st" - apply_helper_script "$bak" /etc/netplan "$log" "$st" /sys/class/net "$rendered" "$AN_S1 $AN_S2 $AN_RC" "$stamp" > "$helper" + apply_helper_script "$bak" /etc/netplan "$log" "$st" /sys/class/net "$rendered" "$AN_S1 $AN_S2 $AN_RC" "$stamp" "$(dirname "$NM_PENDING_CONF")" > "$helper" chmod 0700 "$helper" echo "backup: $bak log: $log" echo "applying from a detached unit (survives an SSH drop); waiting up to $AN_WAIT s ..." @@ -612,11 +718,11 @@ case "${1:-}" in --apply-status-check) # test seam: --apply-status-check prints yes|no (does that status block a new --apply-network run?) apply_status_blocks_run "${2:?status file}" exit 0 ;; - --print-apply-helper) # [conf] the helper script for review; test-only flags: --backup D --netplan-dir D --log L --status S --sysnet N --rendered R --deadlines "s1 s2 rc" --stamp T - shift; pc= a_bak=/var/backups/vanlink-netplan-EXAMPLE a_nd= a_log= a_st= a_sn= a_rf= a_dl= a_stamp= + --print-apply-helper) # [conf] the helper script for review; test-only flags: --backup D --netplan-dir D --log L --status S --sysnet N --rendered R --nm-conf-dir D --deadlines "s1 s2 rc" --stamp T + shift; pc= a_bak=/var/backups/vanlink-netplan-EXAMPLE a_nd= a_log= a_st= a_sn= a_rf= a_dl= a_stamp= a_ncd= while [ $# -gt 0 ]; do case "$1" in - --backup|--netplan-dir|--log|--status|--sysnet|--rendered|--deadlines|--stamp) + --backup|--netplan-dir|--log|--status|--sysnet|--rendered|--nm-conf-dir|--deadlines|--stamp) [ $# -ge 2 ] || die "$1 needs a value" ;; esac case "$1" in @@ -626,22 +732,34 @@ case "${1:-}" in --status) a_st=$2; shift 2 ;; --sysnet) a_sn=$2; shift 2 ;; --rendered) a_rf=$2; shift 2 ;; + --nm-conf-dir) a_ncd=$2; shift 2 ;; --deadlines) a_dl=$2; shift 2 ;; --stamp) a_stamp=$2; shift 2 ;; *) pc=$1; shift ;; esac done load_conf "${pc:-$VANLINK_CONF}" - apply_helper_script "$a_bak" "$a_nd" "$a_log" "$a_st" "$a_sn" "$a_rf" "$a_dl" "$a_stamp" + apply_helper_script "$a_bak" "$a_nd" "$a_log" "$a_st" "$a_sn" "$a_rf" "$a_dl" "$a_stamp" "$a_ncd" exit 0 ;; + --apply-preflight) # ./deploy.sh --apply-preflight [conf] — the problems --apply-network would refuse on; exit 1 if any + c=${2:-$VANLINK_CONF} + load_conf "$c" + hn=$(hostname -s) + [ "$(id -u)" = 0 ] || hn=${VANLINK_HOSTNAME:-$hn} # test knob, ignored as root + out=$(apply_preflight_problems "$c" "${VANLINK_SYSNET:-/sys/class/net}" "$hn") + [ -z "$out" ] && exit 0 + echo "$out" + exit 1 ;; --render-wan-netplan) load_conf "$VANLINK_CONF"; render_wan_netplan; exit 0 ;; --wan-plan) # ./deploy.sh --wan-plan [conf] — what the NM WAN provisioning would do; changes nothing load_conf "${2:-$VANLINK_CONF}" echo "wan-plan: ENABLE_NM_WAN=$ENABLE_NM_WAN" if [ "$ENABLE_NM_WAN" = 1 ]; then if [ -n "${WIFI_WAN_IFACE:-}" ]; then echo " wifi WAN: $WIFI_WAN_IFACE"; else echo " wifi WAN: none (wifis stanza omitted)"; fi - if dpkg -s network-manager >/dev/null 2>&1; then echo " packages: network-manager installed"; else echo " packages: network-manager would install"; fi - echo " netplan: /etc/netplan/50-van-wan.yaml $(nm_wan_state_netplan)" + if nm_pkg_installed; then echo " packages: network-manager installed"; else echo " packages: network-manager would install"; fi + np=$(nm_wan_state_netplan) + if [ "$np" = unreadable ]; then echo " netplan: /etc/netplan/50-van-wan.yaml unreadable (run with sudo)"; else echo " netplan: /etc/netplan/50-van-wan.yaml $np"; fi + echo " uplink NM pending conf: $(nm_pending_action "$np" "$NM_PENDING_CONF")" if [ -f /etc/netplan/10-dhcp-all-interfaces.yaml ]; then echo " armbian: /etc/netplan/10-dhcp-all-interfaces.yaml present -> would move aside"; else echo " armbian: /etc/netplan/10-dhcp-all-interfaces.yaml absent"; fi if [ -z "${CELLULAR_APN:-}" ]; then echo " cellular profile: skipped (no CELLULAR_APN)" @@ -975,7 +1093,16 @@ if [ "$ENABLE_NM_WAN" = 1 ]; then echo "== networkmanager WANs ==" # In place before the package starts NM: no auto-created "Wired connection" profiles. install -D -m0644 ap/nm-no-auto-default.conf /etc/NetworkManager/conf.d/00-van-no-auto-default.conf - if ! dpkg -s network-manager >/dev/null 2>&1; then + # Also in place before the package starts NM: NM keeps off the uplink port until --apply-network hands it + # over (WAN netplan missing), and the conf goes once the netplan matches; see nm_pending_action. NM picks + # up either change at the `nmcli general reload conf` below. + nm_np_state=$(nm_wan_state_netplan) + if [ "$nm_np_state" = missing ]; then + install_rendered ap/nm-uplink-pending.conf "$NM_PENDING_CONF" + elif [ "$nm_np_state" = matches ]; then + rm -f "$NM_PENDING_CONF" + fi + if ! nm_pkg_installed; then # Installed only now: the package starts NM, which manages Wi-Fi/gsm devices by # default, so van-ap-unmanaged.conf (AP radios, LAN ports) must already be in place. apt-get update -qq || true @@ -1003,7 +1130,6 @@ if [ "$ENABLE_NM_WAN" = 1 ]; then echo " created cellular connection ${CELLULAR_CONN_NAME} (apn ${CELLULAR_APN}) — no modem needed" fi fi - nm_np_state=$(nm_wan_state_netplan) if [ "$nm_np_state" != matches ]; then warn "WAN netplan is $nm_np_state (not provisioned/up to date): run ./deploy.sh --apply-network (backs up /etc/netplan and rolls back automatically if the uplink does not come back)" fi diff --git a/tests/check-templates.sh b/tests/check-templates.sh index 8cb2d0c..b9a0ea8 100755 --- a/tests/check-templates.sh +++ b/tests/check-templates.sh @@ -35,8 +35,8 @@ 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) +# vanrouter.js: installed by the loop in the cockpit block; 50-van-wan.yaml: installed only by +# the detached helper of `./deploy.sh --apply-network` (a routine deploy only warns when it differs) # 50-van-wan-wifi.yaml, cellular.nmconnection: installed by dedicated code, not `install_rendered /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 @@ -266,9 +266,16 @@ elif [ -n "$h" ]; then 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 + for c in netplan nmcli ip ping systemctl reboot networkctl; do printf '#!/bin/bash\necho "%s $*" >> "$STUB_CALLS"\n' "$c" > "$stubs/$c" done + cat >> "$stubs/networkctl" <<'STUB' +case " $* " in + *" list "*) echo " 3 enP2p33s0 ether routable configured" ;; + *" status "*) echo "STUB-NETWORKCTL-STATUS" ;; +esac +exit 0 +STUB cat >> "$stubs/netplan" <<'STUB' if [ "$1" = apply ]; then if [ -e "${STUB_HOLD:-/nonexistent}" ]; then @@ -288,14 +295,25 @@ case "$2" in 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}" ;; + GENERAL.STATE,GENERAL.CONNECTION,IP4.ADDRESS,IP4.GATEWAY) echo "STUB-DIAG-NM" ;; esac exit 0 STUB + # STUB_ROUTE: yes | after-rollback | none; STUB_ROUTE_PROTO: static (van-failover's own route, default) | dhcp (a leftover networkd route) cat >> "$stubs/ip" <<'STUB' -if [ "$2" = get ]; then echo "1.1.1.1 via 192.168.100.1 dev x"; exit 0; fi +case "$*" in + "route get "*) echo "1.1.1.1 via 192.168.100.1 dev x"; exit 0 ;; + "-4 addr show "*) echo "STUB-ADDR inet 192.168.100.10/24"; exit 0 ;; +esac +r= 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" ;; + yes) r=1 ;; + after-rollback) [ -e "$STUB_ND/50-van-wan.yaml" ] || r=1 ;; +esac +[ -n "$r" ] || exit 0 +case "$*" in + *"proto static"*) [ "${STUB_ROUTE_PROTO:-static}" = static ] && echo "default via 192.168.100.1 dev enP2p33s0 proto static metric 200" ;; + *) echo "default via 192.168.100.1 dev enP2p33s0 proto ${STUB_ROUTE_PROTO:-static} metric 200" ;; esac exit 0 STUB @@ -305,26 +323,32 @@ 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" + printf '#!/bin/bash\n[ "$1" = cat ] && [ -n "${STUB_NO_BR0:-}" ] && exit 1\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 + for c in netplan nmcli ip ping systemctl reboot networkctl 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" + sb=$(mktemp -d); mkdir -p "$sb/netplan" "$sb/sysnet" "$sb/nmconf" 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" + # the uplink-pending NM conf, and its copy in the backup (as apply_network makes it) + if [ -z "${AN_NOPEND:-}" ]; then + printf '[keyfile]\nunmanaged-devices+=interface-name:enP2p33s0\n' > "$sb/nmconf/zz-van-wan-pending.conf" + cp -a "$sb/nmconf/zz-van-wan-pending.conf" "$sb/bak/nm-pending.conf" + fi [ -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" + --nm-conf-dir "$sb/nmconf" --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; } + grep -qF "nd='$sb/netplan'" "$sb/helper.sh" && grep -qF "ncd='$sb/nmconf'" "$sb/helper.sh" && [ "$(id -u)" != 0 ] \ + || { bad "helper does not bake the sandbox netplan/NM conf dirs (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; } @@ -342,6 +366,23 @@ STUB 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" + [ ! -e "$sb/nmconf/zz-van-wan-pending.conf" ] && ok "apply helper success: uplink-pending NM conf removed" || bad "apply helper success: uplink-pending NM conf left in place" + [ ! -e "$sb/netplan/nm-pending.conf" ] && ok "apply helper success: no nm-pending.conf in the netplan dir" || bad "apply helper success: nm-pending.conf leaked into the netplan dir" + # order in the call log: NM conf reload -> netplan generate -> netplan apply -> van-br0-setup restart -> van-failover restart + ln_of() { grep -n -m1 -- "$1" "$sb/calls" 2>/dev/null | cut -d: -f1 || true; } + l_rl=$(ln_of '^nmcli general reload conf$'); l_gen=$(ln_of '^netplan generate'); l_ap=$(ln_of '^netplan apply') + l_br=$(ln_of '^systemctl restart van-br0-setup$'); l_fo=$(ln_of '^systemctl restart van-failover') + [ -n "$l_rl" ] && [ -n "$l_gen" ] && [ "$l_rl" -lt "$l_gen" ] && ok "apply helper success: NM conf reloaded before netplan generate" || bad "apply helper success: NM conf reload missing or after netplan generate (reload '$l_rl', generate '$l_gen')" + [ -n "$l_br" ] && [ -n "$l_ap" ] && [ -n "$l_fo" ] && [ "$l_ap" -lt "$l_br" ] && [ "$l_br" -lt "$l_fo" ] && ok "apply helper success: van-br0-setup restarted after netplan apply, before stage 2" || bad "apply helper success: van-br0-setup restart missing or misplaced (apply '$l_ap', br0 '$l_br', failover '$l_fo')" + [ "$(grep -c '^systemctl restart van-br0-setup$' "$sb/calls")" = 1 ] && ok "apply helper success: van-br0-setup restarted once" || bad "apply helper success: van-br0-setup restart count" + grep -q '^networkd (info, not gating): *3 enP2p33s0 ether routable configured' "$sb/log" && ok "apply helper success: networkd state of the port logged" || bad "apply helper success: networkd info line missing" + grep -q -- '--- diagnostics' "$sb/log" && bad "apply helper success: diagnostics must only be logged on a failed gate" || ok "apply helper success: no failure diagnostics" + rm -rf "$sb" + # (1b) van-br0-setup unit absent: not restarted, still ok; no pending conf: nothing to reload + AN_NOPEND=1 an_run STUB_NO_BR0=1 + a_is "no van-br0-setup unit: status" "$(st_word)" ok + grep -q '^systemctl restart van-br0-setup' "$sb/calls" && bad "apply helper: restarted a van-br0-setup that does not exist" || ok "apply helper: van-br0-setup restart skipped when the unit is absent" + grep -q '^nmcli general reload conf$' "$sb/calls" && bad "apply helper: reloaded NM conf with no pending conf" || ok "apply helper: no NM conf reload without a pending conf" rm -rf "$sb" # (2) stage 1 fails: connected (externally) an_run NM_STATE="100 (connected (externally))" @@ -349,6 +390,22 @@ STUB 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" + cmp -s "$sb/nmconf/zz-van-wan-pending.conf" "$sb/bak/nm-pending.conf" && ok "apply helper stage1: uplink-pending NM conf restored" || bad "apply helper stage1: uplink-pending NM conf not restored" + [ ! -e "$sb/netplan/nm-pending.conf" ] && ok "apply helper stage1: nm-pending.conf not copied into the netplan dir" || bad "apply helper stage1: nm-pending.conf leaked into the netplan dir" + l_ap2=$(grep -n '^netplan apply' "$sb/calls" | sed -n 2p | cut -d: -f1 || true); l_rl2=$(grep -n '^nmcli general reload conf$' "$sb/calls" | sed -n 2p | cut -d: -f1 || true) + [ -n "$l_ap2" ] && [ -n "$l_rl2" ] && [ "$l_ap2" -lt "$l_rl2" ] && ok "apply helper stage1: NM conf reloaded after the rollback apply" || bad "apply helper stage1: no NM conf reload after the rollback apply (apply '$l_ap2', reload '$l_rl2')" + for m in '--- diagnostics: stage 1' 'STUB-DIAG-NM' 'routers = 192.168.100.1' 'STUB-NETWORKCTL-STATUS' 'STUB-ADDR inet' 'default via 192.168.100.1 dev enP2p33s0'; do + sed -n '/--- diagnostics/,/--- end diagnostics/p' "$sb/log" | grep -qF -- "$m" && ok "apply helper stage1: diagnostics contain '$m'" || bad "apply helper stage1: diagnostics lack '$m'" + done + d_ln=$(grep -n -m1 -- '--- diagnostics' "$sb/log" | cut -d: -f1 || true); r_ln=$(grep -n -m1 'restoring' "$sb/log" | cut -d: -f1 || true) + [ -n "$d_ln" ] && [ -n "$r_ln" ] && [ "$d_ln" -lt "$r_ln" ] && ok "apply helper stage1: diagnostics logged before the restore" || bad "apply helper stage1: diagnostics not before the restore" + grep -q '^networkctl .*status enP2p33s0' "$sb/calls" && ok "apply helper: networkctl status called" || bad "apply helper: networkctl status not called" + grep '^networkctl .*status' "$sb/calls" | grep -qv -- ' -n 0 ' && bad "apply helper: networkctl status must pass -n 0 (no journal)" || ok "apply helper: networkctl status without journal lines" + rm -rf "$sb" + # (2c) stage 1 fails on a host without a pending conf: nothing to restore there + AN_NOPEND=1 an_run NM_STATE=bad + a_is "stage1 without pending conf: status" "$(st_word)" rolled-back + [ ! -e "$sb/nmconf/zz-van-wan-pending.conf" ] && ok "apply helper stage1 without pending conf: none created" || bad "apply helper stage1 without pending conf: one appeared" 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" @@ -383,6 +440,15 @@ STUB 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" + # (5b) stage 2: a default route that is only a leftover networkd/DHCP one (not van-failover's proto static) must not pass + an_run STUB_ROUTE_PROTO=dhcp + a_is "stage2 dhcp-only default route: exit 1" "$rc" 1; a_is "stage2 dhcp-only default route: status" "$(st_word)" rolled-back + grep -q '^ip route show default dev enP2p33s0 proto static$' "$sb/calls" && ok "apply helper stage2: checks van-failover's proto static route on the port" || bad "apply helper stage2: no proto static route check" + grep -qF -- '--- diagnostics: stage 2' "$sb/log" && ok "apply helper stage2: diagnostics logged" || bad "apply helper stage2: diagnostics missing" + [ -e "$sb/netplan/10-dhcp-all-interfaces.yaml" ] && cmp -s "$sb/nmconf/zz-van-wan-pending.conf" "$sb/bak/nm-pending.conf" && ok "apply helper stage2: Armbian file and pending conf restored" || bad "apply helper stage2: Armbian file / pending conf not restored" + rm -rf "$sb" + # (5c) port absent: stage 2 needs no proto static route on it + AN_NOPORT=1 an_run STUB_ROUTE_PROTO=dhcp; a_is "port absent, dhcp route: status" "$(st_word)" ok; 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 @@ -421,6 +487,10 @@ STUB [ -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" + dg=$(echo "$h" | sed -n '/^diag() {/,/^}/p') + [ -n "$dg" ] && ! echo "$dg" | grep -E '(^ *|[;|&(] *)(nmcli|networkctl|ip) ' | grep -q . && [ "$(echo "$dg" | grep -c 'timeout 5 ')" -ge 5 ] \ + && ok "apply helper: every diagnostic command runs under timeout 5" || bad "apply helper: diag() missing or a diagnostic command without timeout 5" + echo "$h" | grep -q journalctl && bad "apply helper must not call journalctl" || ok "apply helper: no journal calls" fi # 18b. status file gate and STARLINK_IFACE validation gs=$(mktemp -d) @@ -501,6 +571,18 @@ echo "$o" | grep -q -- "-> post-deploy: NetworkManager connection $conn not foun echo "$o" | grep -q -- '-> post-deploy: WAN netplan differs' && ok "19e: netplan differs -> warn" || bad "19e: netplan warn missing" printf 'id=%s\n' "$conn" > "$tc/nm/x.nmconnection" nm | grep -q " ok NetworkManager connection $conn exists" && ok "19e: cellular keyfile scan -> ok" || bad "19e: keyfile scan ok missing" +# 19g. an installed WAN netplan this user cannot read (the helper installs it 0600) is "unreadable", not "differs" +if [ "$(id -u)" = 0 ]; then + ok "19g: unreadable-netplan checks skipped (running as root)" +else + chmod 000 "$tc/np.yaml" + o=$(nm) + echo "$o" | grep -qxF ' ok WAN netplan: cannot read as non-root (run with sudo to verify)' && ok "19g: post-check unreadable netplan -> ok line" || bad "19g: post-check unreadable netplan ok line missing" + echo "$o" | grep -q -- '-> post-deploy: WAN netplan' && bad "19g: post-check must not warn about an unreadable netplan" || ok "19g: post-check does not warn about an unreadable netplan" + p=$(VANLINK_WAN_NETPLAN="$tc/np.yaml" ./deploy.sh --wan-plan hosts/van.conf 2>&1) + echo "$p" | grep -qxF ' netplan: /etc/netplan/50-van-wan.yaml unreadable (run with sudo)' && ok "19g: wan-plan unreadable netplan" || bad "19g: wan-plan unreadable netplan line" + chmod 600 "$tc/np.yaml" +fi # 19f. VANLINK_WAN_NETPLAN is a read-only-path test knob: apply_network must neutralise it, and no apply code reads other VANLINK_* vars an=$(sed -n '/^apply_network() {/,/^}/p' deploy.sh) echo "$an" | grep -q "VANLINK_WAN_NETPLAN='' nm_wan_state_netplan" && ! echo "$an" | grep -E 'nm_wan_state_netplan' | grep -qv "VANLINK_WAN_NETPLAN=''" \ @@ -508,4 +590,66 @@ echo "$an" | grep -q "VANLINK_WAN_NETPLAN='' nm_wan_state_netplan" && ! echo "$a if { echo "$an"; sed -n '/^apply_helper_script() {/,/^}/p' deploy.sh; } | grep 'VANLINK_' | grep -vE 'VANLINK_CONF|VANLINK_WAN_NETPLAN=' | grep -q .; then bad "19f: apply path reads a VANLINK_* variable"; else ok "19f: apply path reads no VANLINK_* variable besides VANLINK_CONF"; fi rm -rf "$tc" +# 20. final-review fixes +t20=$(mktemp -d) +# 20a. (M-7) the files keeping NM off the AP/LAN ports and the uplink are installed before the package that starts NM +l_pkg=$(grep -n 'apt-get install -y network-manager' deploy.sh | head -1 | cut -d: -f1 || true) +for pat in 'ap/van-ap-unmanaged.conf' 'ap/nm-no-auto-default.conf' 'ap/nm-uplink-pending.conf'; do + l=$(grep -n "^[[:space:]]*install[_ -].*$pat" deploy.sh | head -1 | cut -d: -f1 || true) + [ -n "$l" ] && [ -n "$l_pkg" ] && [ "$l" -lt "$l_pkg" ] && ok "20a: $pat installed (line $l) before network-manager (line $l_pkg)" || bad "20a: $pat install line '$l' must precede the network-manager install line '$l_pkg'" +done +# 20b. (M-7) read-only sub-commands never call apt-get +mkdir -p "$t20/apt" +printf '#!/bin/sh\necho "apt-get $*" >> "$APT_LOG"\nexit 1\n' > "$t20/apt/apt-get"; chmod +x "$t20/apt/apt-get" +for sub in --check --diff --post-check --wan-plan --print-apply-helper; do + PATH="$t20/apt:$PATH" APT_LOG="$t20/apt.log" POSTCHECK_WAIT=0 ./deploy.sh "$sub" hosts/van.conf >/dev/null 2>&1 || true +done +[ ! -s "$t20/apt.log" ] && ok "20b: --check/--diff/--post-check/--wan-plan/--print-apply-helper never call apt-get" || { bad "20b: a read-only sub-command called apt-get"; sed 's/^/ /' "$t20/apt.log"; } +# 20c. (M-3) network-manager is detected with dpkg-query's Status, in one helper +grep -qE 'dpkg -s network-manager' deploy.sh && bad "20c: deploy.sh still uses dpkg -s network-manager" || ok "20c: no dpkg -s network-manager" +[ "$(grep -c 'nm_pkg_installed' deploy.sh)" -ge 4 ] && grep -q "dpkg-query -W -f='\${Status}' network-manager" deploy.sh && ok "20c: nm_pkg_installed (dpkg-query) used by deploy, --wan-plan and --apply-network" || bad "20c: nm_pkg_installed missing or not used in all three places" +# 20d. (I-2) uplink-pending NM conf template and its --wan-plan line +[ "$(grep -v '^[[:space:]]*#' ap/nm-uplink-pending.conf 2>/dev/null | grep -v '^[[:space:]]*$')" = "$(printf '[keyfile]\nunmanaged-devices+=interface-name:@STARLINK_IFACE@')" ] && ok "20d: nm-uplink-pending.conf is [keyfile] + unmanaged-devices+=" || bad "20d: ap/nm-uplink-pending.conf missing or wrong" +expect_line van ap/nm-uplink-pending.conf 'unmanaged-devices+=interface-name:enP2p33s0' +if [ -e /etc/NetworkManager/conf.d/zz-van-wan-pending.conf ]; then + ok "20d: wan-plan pending-conf assertions skipped (this machine has the pending conf)" +else + VANLINK_CONF=hosts/van.conf ./deploy.sh --render-wan-netplan > "$t20/np.yaml" + p=$(VANLINK_WAN_NETPLAN="$t20/none.yaml" ./deploy.sh --wan-plan hosts/van.conf 2>&1) + echo "$p" | grep -qxF ' uplink NM pending conf: would install' && ok "20d: netplan missing -> pending conf would install" || bad "20d: netplan missing -> 'would install' expected" + p=$(VANLINK_WAN_NETPLAN="$t20/np.yaml" ./deploy.sh --wan-plan hosts/van.conf 2>&1) + echo "$p" | grep -qxF ' uplink NM pending conf: absent' && ok "20d: netplan matches -> pending conf absent" || bad "20d: netplan matches -> 'absent' expected" + echo '# differs' >> "$t20/np.yaml" + p=$(VANLINK_WAN_NETPLAN="$t20/np.yaml" ./deploy.sh --wan-plan hosts/van.conf 2>&1) + echo "$p" | grep -qxF ' uplink NM pending conf: absent' && ok "20d: netplan differs (already handed to NM) -> pending conf not installed" || bad "20d: netplan differs -> 'absent' expected" +fi +# 20e. (I-4) apply preflight: port present and conf named after this host +mkdir -p "$t20/sys/enP2p33s0" "$t20/nosys" +pf() { VANLINK_SYSNET="$1" VANLINK_HOSTNAME="$2" ./deploy.sh --apply-preflight "$3" 2>&1; } +o=$(pf "$t20/sys" van hosts/van.conf) && rc=0 || rc=$? +[ -z "$o" ] && [ "$rc" = 0 ] && ok "20e: preflight fine -> no output, exit 0" || bad "20e: preflight fine gave '$o' (exit $rc)" +o=$(pf "$t20/sys" wan hosts/van.conf) && rc=0 || rc=$? +[ "$rc" = 1 ] && [ "$(echo "$o" | grep -c .)" = 1 ] && echo "$o" | grep -q 'van.conf.*hostname wan' && ok "20e: preflight wrong host -> one problem" || bad "20e: preflight wrong host gave '$o' (exit $rc)" +o=$(pf "$t20/nosys" van hosts/van.conf) && rc=0 || rc=$? +[ "$rc" = 1 ] && [ "$(echo "$o" | grep -c .)" = 1 ] && echo "$o" | grep -q 'enP2p33s0' && ok "20e: preflight absent port -> one problem" || bad "20e: preflight absent port gave '$o' (exit $rc)" +cp hosts/van.conf "$t20/other.conf" +o=$(pf "$t20/sys" wan "$t20/other.conf") && rc=0 || rc=$? +[ -z "$o" ] && [ "$rc" = 0 ] && ok "20e: preflight skips the host-name check for a conf outside hosts/" || bad "20e: preflight conf outside hosts/ gave '$o' (exit $rc)" +an=$(sed -n '/^apply_network() {/,/^}/p' deploy.sh) +echo "$an" | grep -q 'apply_preflight_problems "$VANLINK_CONF" /sys/class/net "$(hostname -s)"' && ok "20e: apply_network runs the preflight on the real sysfs/hostname" || bad "20e: apply_network does not call apply_preflight_problems with real paths" +# 20f. (M-8) a differing installed netplan is shown redacted and refused without --force +psk=$(sed -n 's/^ *password: *"\(.*\)"$/\1/p' ap/50-van-wan-wifi.yaml || true) +d=$(diff -u /dev/null <(VANLINK_CONF=hosts/wan.conf ./deploy.sh --render-wan-netplan) | ./deploy.sh --redact) || true +[ -n "$psk" ] && ! echo "$d" | grep -qF -- "$psk" && echo "$d" | grep -qE '^\+ +password: $' && ok "20f: --redact masks the netplan Wi-Fi password in a diff" || bad "20f: netplan Wi-Fi password not redacted" +echo "$an" | grep -q 'diff -u /etc/netplan/50-van-wan.yaml <(render_wan_netplan) | redact_secrets' && echo "$an" | grep -q 'without --force' && ok "20f: apply_network prints a redacted diff and refuses a differing netplan without --force" || bad "20f: apply_network differs handling" +# 20g. (M-6) root ignores VANLINK_NM_DIR +cpe=$(sed -n '/^cellular_profile_exists() {/,/^}/p' deploy.sh) +grep -q '"$(id -u)" = 0 ] || d=${VANLINK_NM_DIR' <<<"$cpe" && ok "20g: cellular_profile_exists ignores VANLINK_NM_DIR as root" || bad "20g: cellular_profile_exists must ignore VANLINK_NM_DIR as root" +# 20h. (M-5) documented overrides +hdr=$(sed -n '1,/^set -euo pipefail/{/^#/p}' deploy.sh) +for k in 'VANLINK_CONF=' 'VANLINK_HOSTNAME=' '--apply-preflight'; do + grep -qF -- "$k" <<<"$hdr" && ok "20h: header documents $k" || bad "20h: header lacks $k" +done +rm -rf "$t20" + exit "$fail"