deploy: feature flags, --diff, tolerate failed restarts

Li3, NVMe watchdog, Home Assistant/ESPHome, Frigate and Cockpit are
installed, enabled and restarted only when their ENABLE_* flag is 1.
--diff shows what would change in /etc. A unit that fails to restart is
recorded as a warning instead of aborting the deploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 14:34:13 -04:00
co-authored by Claude Sonnet 5
parent d704069ffc
commit 83974f731d
2 changed files with 107 additions and 35 deletions
+48 -4
View File
@@ -105,6 +105,17 @@ case "${1:-}" in
rm -f "$tmp" rm -f "$tmp"
[ "$rc" -eq 0 ] && echo "check OK: $(template_files | wc -l) templates render cleanly" [ "$rc" -eq 0 ] && echo "check OK: $(template_files | wc -l) templates render cleanly"
exit "$rc" ;; exit "$rc" ;;
--diff) # ./deploy.sh --diff [conf] — what would change in /etc, without installing
load_conf "${2:-$VANLINK_CONF}"
grep -E '^[[:space:]]*install_rendered +[^" ]+ +/' deploy.sh | while read -r _ src dst _; do
if [ ! -e "$dst" ]; then
echo "NEW $dst"
elif ! diff -q <(render "$src") "$dst" >/dev/null; then
echo "CHANGED $dst"
diff -u "$dst" <(render "$src") | sed 's/^/ /' || true
fi
done
exit 0 ;;
render) render)
[ -n "${2:-}" ] || { echo "Usage: $0 render <file>"; exit 1; } [ -n "${2:-}" ] || { echo "Usage: $0 render <file>"; exit 1; }
load_conf "$VANLINK_CONF" load_conf "$VANLINK_CONF"
@@ -135,8 +146,10 @@ REQUIRED_PACKAGES=(
python3-paho-mqtt python3-paho-mqtt
mobile-broadband-provider-info mobile-broadband-provider-info
zerotier-one zerotier-one
podman
) )
if [ "$ENABLE_HA" = 1 ]; then
REQUIRED_PACKAGES+=(podman)
fi
OPTIONAL_PACKAGES=( OPTIONAL_PACKAGES=(
) )
for pkg in "${REQUIRED_PACKAGES[@]}"; do for pkg in "${REQUIRED_PACKAGES[@]}"; do
@@ -175,6 +188,12 @@ write_warnings() {
} > "$WARNINGS_FILE" } > "$WARNINGS_FILE"
} }
# A host file with uncommitted edits means this deploy uses unreviewed values.
conf_real=$(readlink -f "$VANLINK_CONF")
if git -c safe.directory='*' -C "$(dirname "$conf_real")" status --porcelain -- "$conf_real" 2>/dev/null | grep -q .; then
warn "$conf_real has uncommitted changes — commit it so the host config stays reviewable"
fi
echo "== netplan drift check ==" echo "== netplan drift check =="
# Netplan is never installed by this script (applying it flaps the uplinks — # Netplan is never installed by this script (applying it flaps the uplinks —
# see note below), so it's easy to edit ap/50-van-wan.yaml and forget the # see note below), so it's easy to edit ap/50-van-wan.yaml and forget the
@@ -321,7 +340,6 @@ python3 -c 'import paho.mqtt' 2>/dev/null \
install -D -m0644 router-status/mosquitto-listener.conf /etc/mosquitto/conf.d/van-listener.conf install -D -m0644 router-status/mosquitto-listener.conf /etc/mosquitto/conf.d/van-listener.conf
# Restart limit drop-ins: prevent infinite restart loops that trigger hardware watchdog # Restart limit drop-ins: prevent infinite restart loops that trigger hardware watchdog
install -D -m0644 router-status/van-gps-owntracks-restart-limit.conf /etc/systemd/system/van-gps-owntracks.service.d/restart-limit.conf install -D -m0644 router-status/van-gps-owntracks-restart-limit.conf /etc/systemd/system/van-gps-owntracks.service.d/restart-limit.conf
install -D -m0644 li3/van-li3-battery-restart-limit.conf /etc/systemd/system/van-li3-battery.service.d/restart-limit.conf
echo "== gps (cellular modem GNSS -> gpsd) ==" echo "== gps (cellular modem GNSS -> gpsd) =="
if dpkg -s gpsd >/dev/null 2>&1; then if dpkg -s gpsd >/dev/null 2>&1; then
@@ -344,6 +362,7 @@ python3 -c 'import gps' 2>/dev/null \
python3 -c 'import paho.mqtt' 2>/dev/null \ python3 -c 'import paho.mqtt' 2>/dev/null \
|| warn "python3-paho-mqtt missing (apt install python3-paho-mqtt) — van-gps-owntracks won't start" || warn "python3-paho-mqtt missing (apt install python3-paho-mqtt) — van-gps-owntracks won't start"
if [ "$ENABLE_LI3" = 1 ]; then
echo "== li3 (RV house battery BMS -> MQTT/HA) ==" echo "== li3 (RV house battery BMS -> MQTT/HA) =="
# NOT the laptop's own AC/battery monitor — that's power/van-battery (different # NOT the laptop's own AC/battery monitor — that's power/van-battery (different
# hardware, different concern). This is the Lithionics Li3 12V LiFePO4 house # hardware, different concern). This is the Lithionics Li3 12V LiFePO4 house
@@ -352,12 +371,14 @@ echo "== li3 (RV house battery BMS -> MQTT/HA) =="
# li3/config.json.example — same pattern as gps/pushover). # li3/config.json.example — same pattern as gps/pushover).
install -D -m0755 li3/van-li3-battery /usr/local/sbin/van-li3-battery install -D -m0755 li3/van-li3-battery /usr/local/sbin/van-li3-battery
install -D -m0644 li3/van-li3-battery.service /etc/systemd/system/van-li3-battery.service install -D -m0644 li3/van-li3-battery.service /etc/systemd/system/van-li3-battery.service
install -D -m0644 li3/van-li3-battery-restart-limit.conf /etc/systemd/system/van-li3-battery.service.d/restart-limit.conf
if [ ! -f /etc/van-li3/config.json ]; then if [ ! -f /etc/van-li3/config.json ]; then
install -D -m0600 li3/config.json.example /etc/van-li3/config.json install -D -m0600 li3/config.json.example /etc/van-li3/config.json
warn "seeded /etc/van-li3/config.json (EDIT IT: add MQTT username + password)" warn "seeded /etc/van-li3/config.json (EDIT IT: add MQTT username + password)"
fi fi
python3 -c 'import bleak' 2>/dev/null \ python3 -c 'import bleak' 2>/dev/null \
|| warn "python3-bleak missing (apt install python3-bleak) — van-li3-battery won't start" || warn "python3-bleak missing (apt install python3-bleak) — van-li3-battery won't start"
fi
echo "== thermal monitor ==" echo "== thermal monitor =="
install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal
@@ -370,24 +391,30 @@ if [ ! -f /etc/van-battery/pushover.json ]; then
warn "seeded /etc/van-battery/pushover.json (EDIT IT: add Pushover token + user)" warn "seeded /etc/van-battery/pushover.json (EDIT IT: add Pushover token + user)"
fi fi
if [ "$ENABLE_NVME_WATCH" = 1 ]; then
echo "== nvme watchdog ==" echo "== nvme watchdog =="
install -D -m0755 power/van-nvme-watch /usr/local/sbin/van-nvme-watch install -D -m0755 power/van-nvme-watch /usr/local/sbin/van-nvme-watch
install_rendered power/nvme-watch-config.json /etc/van-nvme-watch/config.json install_rendered power/nvme-watch-config.json /etc/van-nvme-watch/config.json
install -D -m0644 power/van-nvme-watch.service /etc/systemd/system/van-nvme-watch.service install -D -m0644 power/van-nvme-watch.service /etc/systemd/system/van-nvme-watch.service
fi
echo "== home assistant ==" echo "== home assistant =="
# Native HA (Podman Quadlet, replaced the ha_van VM). daemon-reload below # Native HA (Podman Quadlet, replaced the ha_van VM). daemon-reload below
# regenerates homeassistant.service; started (not restarted) at the end so a # regenerates homeassistant.service; started (not restarted) at the end so a
# deploy never bounces HA — after editing the .container, restart it manually. # deploy never bounces HA — after editing the .container, restart it manually.
if [ "$ENABLE_HA" = 1 ]; then
install -D -m0644 ha/homeassistant.container /etc/containers/systemd/homeassistant.container install -D -m0644 ha/homeassistant.container /etc/containers/systemd/homeassistant.container
install -d -m0755 /srv/homeassistant install -d -m0755 /srv/homeassistant
# ESPHome dashboard (sibling container, same rationale as HA above — no # ESPHome dashboard (sibling container, same rationale as HA above — no
# Supervisor/add-on store here). # Supervisor/add-on store here).
install -D -m0644 ha/esphome.container /etc/containers/systemd/esphome.container install -D -m0644 ha/esphome.container /etc/containers/systemd/esphome.container
install -d -m0755 /srv/esphome install -d -m0755 /srv/esphome
fi
# Frigate NVR (sibling container, same rationale as HA/ESPHome above). # Frigate NVR (sibling container, same rationale as HA/ESPHome above).
if [ "$ENABLE_FRIGATE" = 1 ]; then
install -D -m0644 ha/frigate.container /etc/containers/systemd/frigate.container install -D -m0644 ha/frigate.container /etc/containers/systemd/frigate.container
install -d -m0755 /srv/frigate/config /srv/frigate/storage install -d -m0755 /srv/frigate/config /srv/frigate/storage
fi
echo "== hardware watchdog ==" echo "== hardware watchdog =="
install -D -m0644 power/10-vanlink-watchdog.conf /etc/systemd/system.conf.d/10-vanlink-watchdog.conf install -D -m0644 power/10-vanlink-watchdog.conf /etc/systemd/system.conf.d/10-vanlink-watchdog.conf
@@ -410,24 +437,41 @@ systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
systemctl unmask hostapd >/dev/null 2>&1 || true systemctl unmask hostapd >/dev/null 2>&1 || true
# Disable system dnsmasq.service to avoid port 53 conflicts — van-ap-dnsmasq owns AP DHCP # Disable system dnsmasq.service to avoid port 53 conflicts — van-ap-dnsmasq owns AP DHCP
systemctl disable dnsmasq.service >/dev/null 2>&1 || true systemctl disable dnsmasq.service >/dev/null 2>&1 || true
systemctl enable regdomain.service van-br0-setup.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-nvme-watch van-ap-watchdog van-ap-watchdog-2g van-wlan-watchdog van-modem-watch van-sms-watch van-gps-owntracks van-li3-battery van-router-mqtt >/dev/null 2>&1 || true ENABLE_UNITS=(regdomain.service van-br0-setup.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-ap-watchdog van-ap-watchdog-2g van-wlan-watchdog van-modem-watch van-sms-watch van-gps-owntracks van-router-mqtt)
if [ "$ENABLE_NVME_WATCH" = 1 ]; then
ENABLE_UNITS+=(van-nvme-watch)
fi
if [ "$ENABLE_LI3" = 1 ]; then
ENABLE_UNITS+=(van-li3-battery)
fi
systemctl enable "${ENABLE_UNITS[@]}" >/dev/null 2>&1 || true
# bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus # bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus
systemctl enable --now bluetooth >/dev/null 2>&1 || true systemctl enable --now bluetooth >/dev/null 2>&1 || true
if [ "$ENABLE_HA" = 1 ]; then
systemctl start homeassistant || warn "homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)" systemctl start homeassistant || warn "homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)"
systemctl start esphome || warn "esphome failed to start (podman/quadlet — check journalctl -u esphome)" systemctl start esphome || warn "esphome failed to start (podman/quadlet — check journalctl -u esphome)"
fi
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
systemctl restart van-thermal systemctl restart van-thermal
if [ "$ENABLE_NVME_WATCH" = 1 ]; then
systemctl restart van-nvme-watch systemctl restart van-nvme-watch
fi
systemctl restart van-sms-watch systemctl restart van-sms-watch
systemctl restart van-gps-owntracks systemctl restart van-gps-owntracks
if [ "$ENABLE_LI3" = 1 ]; then
systemctl restart van-li3-battery systemctl restart van-li3-battery
fi
systemctl restart van-router-mqtt systemctl restart van-router-mqtt
# 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
# restart in dependency order: bridge + members first, then hostapd enslaves the # restart in dependency order: bridge + members first, then hostapd enslaves the
# radios, then dnsmasq binds br0, then NAT/failover # radios, then dnsmasq binds br0, then NAT/failover
systemctl restart systemd-networkd systemctl restart systemd-networkd
systemctl restart van-ap-dnsmasq nftables van-failover van-wlan-watchdog for u in van-ap-dnsmasq nftables van-failover van-wlan-watchdog; do
systemctl restart "$u" || warn "$u failed to restart (journalctl -xeu $u)"
done
# The AP radios live on the USB hub and may be absent; the start then fails but # The AP radios live on the USB hub and may be absent; the start then fails but
# Restart=always keeps retrying and claims them the moment they enumerate. # Restart=always keeps retrying and claims them the moment they enumerate.
systemctl restart hostapd hostapd-2g \ systemctl restart hostapd hostapd-2g \
+28
View File
@@ -69,4 +69,32 @@ else
ok "van: unmanaged-devices has no eth0" ok "van: unmanaged-devices has no eth0"
fi 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
# 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
exit "$fail" exit "$fail"