deploy: non-fatal service restarts; mosquitto password_file safeguard and post-check probe

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
2026-09-23 22:29:51 -04:00
co-authored by Claude Sonnet 5
parent da59562511
commit 5ce6996716
2 changed files with 83 additions and 10 deletions
+48 -10
View File
@@ -28,6 +28,10 @@
# VANLINK_HOSTNAME=<name> # test override: host name used by --apply-preflight (default `hostname -s`; ignored when run as root) # 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_FAILOVER_STATE=<file> # test override: van-failover state file read by --post-check (default /run/van-failover/state.json)
# VANLINK_SYSLEDS=<dir> # test override: sysfs LED dir read by --post-check (default /sys/class/leds; ignored when run as root) # VANLINK_SYSLEDS=<dir> # test override: sysfs LED dir read by --post-check (default /sys/class/leds; ignored when run as root)
# VANLINK_MOSQUITTO_CONF=<file> # test override: mosquitto.conf read by --post-check / --mosquitto-pwfile-check (default /etc/mosquitto/mosquitto.conf; conf.d is taken
# # beside it; ignored when run as root)
# VANLINK_MOSQUITTO_USER=<name> # test override: the broker's unprivileged user (default mosquitto; ignored when run as root)
# ./deploy.sh --mosquitto-pwfile-check [mosquitto.conf] # read-only: prints the broker's password_file when the mosquitto user cannot read it, else nothing (exit 0)
# VANLINK_APPLY_STATUS=<file> # test override: apply-network status file read by --post-check (default /var/lib/vanlink/apply-network.status) # 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_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;
@@ -256,6 +260,22 @@ classify_radio() { # stdin: `iw dev <if> info` text -> beaconing (has an ssid l
echo silent echo silent
} }
mosquitto_conf() { # path of mosquitto.conf; the test knob is honoured only when not root
local c=/etc/mosquitto/mosquitto.conf
[ "$(id -u)" = 0 ] || c=${VANLINK_MOSQUITTO_CONF:-$c}
echo "$c"
}
mosquitto_pwfile_problem() { # [mosquitto.conf] — prints the password_file path when it exists but the mosquitto user cannot read it; else nothing
local conf=${1:-$(mosquitto_conf)} user=mosquitto pw
[ "$(id -u)" = 0 ] || user=${VANLINK_MOSQUITTO_USER:-$user}
pw=$({ cat "$conf" "$(dirname "$conf")"/conf.d/*.conf 2>/dev/null || true; } | awk '/^[[:space:]]*password_file[[:space:]]/ { print $2 }' | tail -n1) || pw=
[ -n "$pw" ] && [ -e "$pw" ] || return 0
id "$user" >/dev/null 2>&1 && command -v runuser >/dev/null 2>&1 || return 0
runuser -u "$user" -- test -r "$pw" >/dev/null 2>&1 || echo "$pw"
return 0
}
post_check() { # read-only live checks; every probe is guarded, warns instead of aborting post_check() { # read-only live checks; every probe is guarded, warns instead of aborting
set_units set_units
echo "== post-deploy check ==" echo "== post-deploy check =="
@@ -342,6 +362,15 @@ post_check() { # read-only live checks; every probe is guarded, warns instead o
refused) warn "post-deploy: van-router-mqtt is refused by the MQTT broker — add credentials to /etc/van-router-mqtt/credentials.json (see the service journal)" ;; refused) warn "post-deploy: van-router-mqtt is refused by the MQTT broker — add credentials to /etc/van-router-mqtt/credentials.json (see the service journal)" ;;
connected) echo " ok van-router-mqtt: MQTT connection ok" ;; connected) echo " ok van-router-mqtt: MQTT connection ok" ;;
esac esac
# g2b. mosquitto (the base router-status broker): unit active, password file readable by the broker's user
local mu mpw
mu=$(systemctl show -p ActiveState --value mosquitto 2>/dev/null) || mu=
if [ "$mu" = active ]; then echo " ok unit mosquitto active"
else warn "post-deploy: unit mosquitto is not active (ActiveState=${mu:-?})"; fi
mpw=$(mosquitto_pwfile_problem)
if [ -n "$mpw" ]; then
warn "post-deploy: mosquitto password file $mpw is not readable by the mosquitto user — the broker will fail to start on its next restart (fix: chown mosquitto:mosquitto $mpw; chmod 0600 $mpw)"
else echo " ok mosquitto password file readable (or none configured)"; fi
if [ "$ENABLE_LEDS" = 1 ]; then if [ "$ENABLE_LEDS" = 1 ]; then
# g3. front LEDs (van-leds): each configured LED exists; the static ones carry the expected trigger. # g3. front LEDs (van-leds): each configured LED exists; the static ones carry the expected trigger.
# The WAN LED follows the uplink, so its state is only reported, never warned about. # The WAN LED follows the uplink, so its state is only reported, never warned about.
@@ -782,6 +811,9 @@ case "${1:-}" in
[ -e "$path" ] && echo "REMOVE $path" [ -e "$path" ] && echo "REMOVE $path"
done || true done || true
exit 0 ;; exit 0 ;;
--mosquitto-pwfile-check) # ./deploy.sh --mosquitto-pwfile-check [mosquitto.conf] — print the password_file the mosquitto user cannot read (read-only)
mosquitto_pwfile_problem "${2:-}"
exit 0 ;;
--redact) # ./deploy.sh --redact — stdin through redact_secrets (no config, no root) --redact) # ./deploy.sh --redact — stdin through redact_secrets (no config, no root)
redact_secrets redact_secrets
exit 0 ;; exit 0 ;;
@@ -1232,8 +1264,14 @@ systemctl daemon-reexec
# ignore-auto-dns only takes effect on a connection's next activation though — # ignore-auto-dns only takes effect on a connection's next activation though —
# an already-up WAN keeps its currently-applied DNS until it reconnects (or # an already-up WAN keeps its currently-applied DNS until it reconnects (or
# reboot), deliberately not forced here (reconnecting a WAN flaps it). # reboot), deliberately not forced here (reconnecting a WAN flaps it).
systemctl restart systemd-resolved systemctl restart systemd-resolved || warn "systemd-resolved failed to restart (journalctl -xeu systemd-resolved)"
systemctl restart mosquitto # A password_file the mosquitto user cannot read (seen: root:root 0700) makes the restart fail and the broker stay down.
mq_pw=$(mosquitto_pwfile_problem /etc/mosquitto/mosquitto.conf)
if [ -n "$mq_pw" ]; then
chown mosquitto:mosquitto "$mq_pw" && chmod 0600 "$mq_pw"
warn "mosquitto password file $mq_pw was not readable by the mosquitto user — fixed ownership (mosquitto:mosquitto 0600)"
fi
systemctl restart mosquitto || warn "mosquitto failed to restart (journalctl -xeu mosquitto) — MQTT (Home Assistant, Li3, router status) is down"
# networkd here owns only the AP radios + bridge + wired LAN port (no real uplink); # networkd here owns only the AP radios + bridge + wired LAN port (no real uplink);
# its wait-online would just stall network-online.target. NM-wait-online covers WANs. # its wait-online would just stall network-online.target. NM-wait-online covers WANs.
systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
@@ -1251,18 +1289,18 @@ fi
if [ "$ENABLE_FRIGATE" = 1 ]; then if [ "$ENABLE_FRIGATE" = 1 ]; then
systemctl start frigate || warn "frigate failed to start (podman/quadlet — check journalctl -u frigate)" systemctl start frigate || warn "frigate failed to start (podman/quadlet — check journalctl -u frigate)"
fi fi
systemctl restart van-thermal systemctl restart van-thermal || warn "van-thermal failed to restart (journalctl -xeu van-thermal)"
if [ "$ENABLE_NVME_WATCH" = 1 ]; then if [ "$ENABLE_NVME_WATCH" = 1 ]; then
systemctl restart van-nvme-watch systemctl restart van-nvme-watch || warn "van-nvme-watch failed to restart (journalctl -xeu van-nvme-watch)"
fi fi
systemctl restart van-sms-watch systemctl restart van-sms-watch || warn "van-sms-watch failed to restart (journalctl -xeu van-sms-watch)"
systemctl restart van-gps-owntracks systemctl restart van-gps-owntracks || warn "van-gps-owntracks failed to restart (journalctl -xeu van-gps-owntracks)"
if [ "$ENABLE_LI3" = 1 ]; then if [ "$ENABLE_LI3" = 1 ]; then
systemctl restart van-li3-battery systemctl restart van-li3-battery || warn "van-li3-battery failed to restart (journalctl -xeu van-li3-battery)"
fi fi
systemctl restart van-router-mqtt systemctl restart van-router-mqtt || warn "van-router-mqtt failed to restart (journalctl -xeu van-router-mqtt)"
if [ "$ENABLE_LEDS" = 1 ]; then if [ "$ENABLE_LEDS" = 1 ]; then
systemctl restart van-leds systemctl restart van-leds || warn "van-leds failed to restart (journalctl -xeu van-leds)"
fi fi
# Pick up unmanaged-devices changes so NM releases/keeps the right interfaces. # Pick up unmanaged-devices changes so NM releases/keeps the right interfaces.
nmcli general reload 2>/dev/null || systemctl reload NetworkManager 2>/dev/null || true nmcli general reload 2>/dev/null || systemctl reload NetworkManager 2>/dev/null || true
@@ -1276,7 +1314,7 @@ done
# retrying and claims it the moment it enumerates (post_check reports absent radios). # retrying and claims it the moment it enumerates (post_check reports absent radios).
systemctl restart hostapd hostapd-2g || true systemctl restart hostapd hostapd-2g || true
# AP watchdogs last, after hostapd is back up (they only ever restart a wedged hostapd) # AP watchdogs last, after hostapd is back up (they only ever restart a wedged hostapd)
systemctl restart van-ap-watchdog van-ap-watchdog-2g systemctl restart van-ap-watchdog van-ap-watchdog-2g || warn "an AP watchdog failed to restart (journalctl -xeu van-ap-watchdog)"
networkctl reload 2>/dev/null || true networkctl reload 2>/dev/null || true
post_check post_check
+35
View File
@@ -1049,4 +1049,39 @@ o=$(pl "$L" hosts/wan.conf); { ! echo "$o" | grep -q 'LED'; } && ok "22e: ENABLE
grep -qF 'need["van-leds"]="ENABLE_LEDS"' tests/check-templates.sh && ok "22f: check 6 gates van-leds" || bad "22f: check 6 lacks van-leds" grep -qF 'need["van-leds"]="ENABLE_LEDS"' tests/check-templates.sh && ok "22f: check 6 gates van-leds" || bad "22f: check 6 lacks van-leds"
rm -rf "$t22" rm -rf "$t22"
# 23. mosquitto: a non-fatal restart and the password_file safeguard
if grep -nE '^[[:space:]]*systemctl restart mosquitto' deploy.sh | grep -qv '|| warn'; then bad "23a: bare 'systemctl restart mosquitto' (set -e would abort the deploy)"; else ok "23a: every systemctl restart mosquitto has || warn"; fi
if awk '/^set_units\(\)/,0' deploy.sh | sed -n '/^echo "== apply =="/,/^post_check$/p' | grep -E '^[[:space:]]*systemctl restart ' | grep -v '||' | grep -qv 'systemd-networkd$'; then bad "23a: bare systemctl restart in the apply section"; else ok "23a: only the networkd restart is fatal in the apply section"; fi
t23=$(mktemp -d); mkdir -p "$t23/etc/conf.d" "$t23/stub" "$t23/nostub"
echo secret > "$t23/pw"
printf '# password_file /nope\nlistener 1883\npassword_file %s\n' "$t23/pw" > "$t23/etc/mosquitto.conf"
stubru() { printf '#!/bin/sh\n# runuser -u USER -- test -r FILE\nexit %s\n' "$1" > "$t23/stub/runuser"; chmod +x "$t23/stub/runuser"; }
mq() { PATH="$t23/stub:/usr/bin:/bin" VANLINK_MOSQUITTO_USER=root ./deploy.sh --mosquitto-pwfile-check "$@" 2>&1; }
stubru 1; o=$(mq "$t23/etc/mosquitto.conf")
[ "$o" = "$t23/pw" ] && ok "23b: unreadable password file is reported (path only)" || bad "23b: unreadable: got '$o'"
stubru 0; o=$(mq "$t23/etc/mosquitto.conf"); [ -z "$o" ] && ok "23b: readable -> nothing" || bad "23b: readable: got '$o'"
stubru 1; printf 'listener 1883\n#password_file %s\n' "$t23/pw" > "$t23/etc/mosquitto.conf"; o=$(mq "$t23/etc/mosquitto.conf")
[ -z "$o" ] && ok "23b: commented password_file / none -> nothing" || bad "23b: none: got '$o'"
echo "password_file $t23/pw" > "$t23/etc/conf.d/x.conf"; o=$(mq "$t23/etc/mosquitto.conf")
[ "$o" = "$t23/pw" ] && ok "23b: password_file in conf.d is found" || bad "23b: conf.d: got '$o'"
rm "$t23/etc/conf.d/x.conf"; printf 'password_file %s\n' "$t23/gone" > "$t23/etc/mosquitto.conf"; o=$(mq "$t23/etc/mosquitto.conf")
[ -z "$o" ] && ok "23b: nonexistent password file -> nothing" || bad "23b: nonexistent: got '$o'"
printf 'password_file %s\n' "$t23/pw" > "$t23/etc/mosquitto.conf"
o=$(PATH="/usr/bin:/bin" VANLINK_MOSQUITTO_USER=root ./deploy.sh --mosquitto-pwfile-check "$t23/etc/mosquitto.conf" 2>&1)
[ -z "$o" ] && ok "23b: runuser missing -> nothing" || bad "23b: no runuser: got '$o'"
o=$(PATH="$t23/stub:/usr/bin:/bin" VANLINK_MOSQUITTO_USER=no-such-user-x ./deploy.sh --mosquitto-pwfile-check "$t23/etc/mosquitto.conf" 2>&1)
[ -z "$o" ] && ok "23b: unknown user -> nothing" || bad "23b: no user: got '$o'"
pcm() { PATH="$t23/stub:/usr/bin:/bin" VANLINK_MOSQUITTO_CONF="$t23/etc/mosquitto.conf" VANLINK_MOSQUITTO_USER=root POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf 2>&1 || true; }
stubru 1; o=$(pcm)
echo "$o" | grep -qF -- "-> post-deploy: mosquitto password file $t23/pw is not readable by the mosquitto user — the broker will fail to start on its next restart (fix: chown mosquitto:mosquitto $t23/pw; chmod 0600 $t23/pw)" && ok "23c: unreadable -> warn" || bad "23c: unreadable no warn"
echo "$o" | grep -q 'secret' && bad "23c: password file content leaked" || ok "23c: no file content in output"
stubru 0; o=$(pcm)
{ echo "$o" | grep -qF 'ok mosquitto password file readable' && ! echo "$o" | grep -qF 'mosquitto password file'"$t23"; } && ok "23c: readable -> ok" || bad "23c: readable"
: > "$t23/etc/mosquitto.conf"; stubru 1; o=$(pcm)
{ echo "$o" | grep -qF 'ok mosquitto password file readable (or none configured)' && ! echo "$o" | grep -q 'is not readable by the mosquitto user'; } && ok "23c: no password_file -> ok" || bad "23c: no password_file"
printf 'password_file %s\n' "$t23/pw" > "$t23/etc/mosquitto.conf"
o=$(PATH="/usr/bin:/bin" VANLINK_MOSQUITTO_CONF="$t23/etc/mosquitto.conf" VANLINK_MOSQUITTO_USER=root POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf 2>&1 || true)
! echo "$o" | grep -q 'is not readable by the mosquitto user' && ok "23c: runuser missing -> no warn" || bad "23c: runuser missing warned"
rm -rf "$t23"
exit "$fail" exit "$fail"