apply-network: final-review fixes (diagnostics, NM off the uplink until handover, preflight, stricter stage 2)

I-1 helper logs diagnostics (nmcli, DHCP routers, networkctl -n 0, ip addr, ip route; each
    under timeout 5) before a stage 1/2 restore, and the port's networkctl list line on success
    (information only, never gating).
I-2 ap/nm-uplink-pending.conf -> /etc/NetworkManager/conf.d/zz-van-wan-pending.conf keeps NM
    off STARLINK_IFACE while the WAN netplan is missing; installed before network-manager,
    removed once the netplan matches, left as is when it differs (port already handed to NM).
    apply_network backs it up as nm-pending.conf; the helper removes it + reloads NM conf right
    before netplan generate/apply, restore() puts it back and reloads after the re-apply.
    --wan-plan prints "uplink NM pending conf: ...".
I-3 nm_wan_state_netplan: "unreadable" (0600 file, non-root); --wan-plan and post-check say so
    without warning.
I-4 apply_preflight_problems (+ hidden --apply-preflight, VANLINK_HOSTNAME test knob ignored as
    root): port absent / conf of another host -> refused without --force. Stage 2 also needs
    van-failover's own "proto static" default route on the port.
M-1 helper restarts van-br0-setup (if the unit exists) after the forward netplan apply.
M-3 nm_pkg_installed (dpkg-query Status) in deploy, --wan-plan and --apply-network.
M-5 header documents VANLINK_CONF; stale test comment fixed.
M-6 cellular_profile_exists ignores VANLINK_NM_DIR as root.
M-7 tests: NM-off files installed before network-manager; read-only sub-commands never apt-get.
M-8 an existing, differing WAN netplan is shown (redact_secrets now masks YAML password:) and
    refused without --force.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
2026-09-23 19:41:02 -04:00
co-authored by Claude Opus 5.5
parent 5c18bcdad6
commit 3fce6a7907
3 changed files with 320 additions and 41 deletions
+156 -30
View File
@@ -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/<name>.conf with <name> != `hostname -s`
# ./deploy.sh --apply-status-check <file> # 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 <file> # preview a templated file on stdout
# ./deploy.sh --redact # filter stdin, masking secret values (used by --diff; for tests)
# VANLINK_SYSNET=<dir> # test override: sysfs net dir used by --post-check (default /sys/class/net)
# VANLINK_CONF=<file> # host config path (default /etc/vanlink.conf); sub-commands taking [conf] use that argument instead
# VANLINK_SYSNET=<dir> # test override: sysfs net dir used by --post-check and --apply-preflight (default /sys/class/net)
# VANLINK_HOSTNAME=<name> # test override: host name used by --apply-preflight (default `hostname -s`; ignored when run as root)
# VANLINK_FAILOVER_STATE=<file> # test override: van-failover state file read by --post-check (default /run/van-failover/state.json)
# VANLINK_APPLY_STATUS=<file> # test override: apply-network status file read by --post-check (default /var/lib/vanlink/apply-network.status)
# VANLINK_WAN_NETPLAN=<file> # test override: installed WAN netplan compared by nm_wan_state_netplan (default /etc/netplan/50-van-wan.yaml)
# VANLINK_NM_DIR=<dir> # test override: where existing NM *.nmconnection files are looked up (default /etc/NetworkManager/system-connections)
# VANLINK_NM_DIR=<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 <netplan-state> <pending-conf> -> 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 <src> <dst> [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<redacted>/I' \
-e 's/("[A-Za-z_]*(pass(word|phrase)|secret|token|user)[A-Za-z_]*"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"<redacted>"/I'
-e 's/("[A-Za-z_]*(pass(word|phrase)|secret|token|user)[A-Za-z_]*"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"<redacted>"/I' \
-e 's/^([-+ ]?[[:space:]]*(pass(word|phrase)|secret|token)[A-Za-z_]*:[[:space:]]*)[^[:space:]].*/\1<redacted>/I'
}
# apply_preflight_problems <conf> <sysnet-dir> <hostname> — 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/<name>.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 <status-file> -> yes|no:
esac
}
# apply_helper_script <backup-dir> [netplan-dir] [log] [status-file] [sysnet-dir] [rendered-file] [deadlines "s1 s2 rc"] [run-stamp]
# apply_helper_script <backup-dir> [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 <<HELPER
#!/bin/bash
# Generated by deploy.sh --apply-network; runs detached. Installs the rendered WAN netplan,
# applies it, verifies the uplink in two stages and restores the pre-run netplan on any failure
# (including TERM/INT/HUP, e.g. systemd stopping the unit at its RuntimeMaxSec).
# removes the NM conf that kept NM off the uplink ($pc), applies, verifies the uplink in two
# stages and restores the pre-run netplan (and $pc) on any failure (including TERM/INT/HUP,
# e.g. systemd stopping the unit at its RuntimeMaxSec).
# Stage 1 (${d1}s): the apply succeeded and, if the Starlink port exists, NM shows it connected
# via a netplan-* profile with an IPv4 address whose gateway (IP4.GATEWAY, else the DHCP
# routers option: never-default hides IP4.GATEWAY) answers on-link.
# Stage 2 (${d2}s): van-failover restarted; default route and internet ping.
# Stage 2 (${d2}s): van-failover restarted; default route and internet ping, and (port present)
# van-failover's own default route on the port (proto static; the NM profile installs none).
# A failed gate logs diagnostics (NM, DHCP routers, networkd, addresses, routes) before restoring.
bak='$bak'
nd='$nd'
log='$log'
st='$st'
sn='$sn'
rf='$rf'
ncd='$ncd'
exec >>"\$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 <stage>: 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 <reason>: 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 <reason>: 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 <reason>: 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 <status-file> 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