deploy: hardware-instance templating, wlan0 boot watchdog, deploy-time warnings in Cockpit
- deploy.conf templates interface names/USB IDs (@TOKEN@ substitution) across ap/* configs so a dongle swap only needs deploy.conf edited, not the repo configs themselves; drops ap/rtw88.conf (old 2.4GHz dongle retired for the DWA-171, which needs no such power-save override). - failover/van-wlan-watchdog: recovers wlan0 from NM's post-boot no-secrets wedge (a boot-time supplicant race, not a real credential failure). - deploy.sh: warn() collects dependency/config warnings (missing python3-gps, python3-paho-mqtt, mobile-broadband-provider-info, grpcurl, gpsd; netplan drift; unedited example configs) into /var/lib/vanlink/deploy-warnings.json, rendered as an amber Cockpit card so they're visible without reading deploy output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8d5415f326
commit
5d88e1b30c
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""van-wlan-watchdog — recover wlan0 from NetworkManager's post-boot no-secrets wedge.
|
||||
|
||||
Seen on 2026-07-30: wlan0 (onboard radio, used as the WiFi WAN) sometimes fails its
|
||||
very first post-boot association attempt with a spurious supplicant "psk mismatch"
|
||||
(a boot-time brcmfmac firmware/regulatory race, not a real credential problem — a
|
||||
second attempt with the *same* stored secret succeeds immediately). NetworkManager
|
||||
treats any handshake failure it reads as bad secrets as terminal: it does not retry
|
||||
autoconnect after a no-secrets failure, so the device just sits in `disconnected`
|
||||
until something explicitly re-triggers it. This is that trigger.
|
||||
|
||||
Deliberately does NOT hardcode a connection/SSID name: which WiFi network wlan0 uses
|
||||
changes with wherever the van is parked (home, a campsite, a neighbour's AP — see
|
||||
config profiles managed from Cockpit's Networking tab or the Wi-Fi selector on the Van
|
||||
Router page). `nmcli device connect wlan0` lets NM pick amongst whatever profiles are
|
||||
saved and in range on its own, exactly like its normal autoconnect would.
|
||||
|
||||
Bounded to a few minutes after boot, then exits — this is a backstop for the boot race
|
||||
above, not a permanent watcher. It must NOT fight a deliberate later disconnect (e.g.
|
||||
the Cockpit "Disconnect" button), which is why it doesn't loop forever.
|
||||
|
||||
Usage: van-wlan-watchdog [device]
|
||||
Defaults to wlan0 (the AP radios are separate USB dongles, watched by
|
||||
van-ap-watchdog instead).
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
DEVICE = sys.argv[1] if len(sys.argv) > 1 else "wlan0"
|
||||
INTERVAL = 15 # seconds between checks
|
||||
BUDGET_S = 8 * INTERVAL # ~2 minutes of retries after boot, then give up quietly
|
||||
|
||||
|
||||
def log(msg, level="info"):
|
||||
# systemd journal severity prefixes (sd-daemon), same convention as van-ap-watchdog.
|
||||
pri = {"info": "<6>", "warn": "<4>", "crit": "<2>"}.get(level, "<6>")
|
||||
print(pri + msg, flush=True)
|
||||
|
||||
|
||||
def device_state(dev):
|
||||
"""NM's state word for dev (e.g. 'connected', 'disconnected', 'unavailable'), or
|
||||
None if nmcli failed or the device isn't known to NM yet."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["nmcli", "-t", "-f", "GENERAL.STATE", "device", "show", dev],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
).stdout.strip()
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log(f"nmcli device show {dev} failed: {e}", "warn")
|
||||
return None
|
||||
# terse output looks like "GENERAL.STATE:30 (disconnected)"
|
||||
if "(" in out and out.endswith(")"):
|
||||
return out.rsplit("(", 1)[1][:-1]
|
||||
return None
|
||||
|
||||
|
||||
def connect(dev):
|
||||
log(f"{dev} is disconnected — nudging NM to reconnect (autoconnect doesn't retry "
|
||||
f"after a no-secrets failure, only manual/dispatcher re-activation does)", "warn")
|
||||
r = subprocess.run(["nmcli", "device", "connect", dev],
|
||||
capture_output=True, text=True, timeout=45)
|
||||
if r.returncode == 0:
|
||||
log(f"{dev} reconnected")
|
||||
else:
|
||||
log(f"nmcli device connect {dev} returned {r.returncode}: {r.stderr.strip()}", "warn")
|
||||
|
||||
|
||||
def main():
|
||||
log(f"van-wlan-watchdog up: watching {DEVICE} for up to {BUDGET_S}s after boot")
|
||||
elapsed = 0
|
||||
while elapsed < BUDGET_S:
|
||||
state = device_state(DEVICE)
|
||||
if state == "disconnected":
|
||||
connect(DEVICE)
|
||||
elif state == "connected":
|
||||
log(f"{DEVICE} connected — exiting")
|
||||
return
|
||||
# "unavailable" (no known/in-range AP), "unmanaged", or unreadable: nothing to
|
||||
# nudge — retrying nmcli device connect would just fail again.
|
||||
time.sleep(INTERVAL)
|
||||
elapsed += INTERVAL
|
||||
log(f"{DEVICE} still not connected after {BUDGET_S}s — giving up "
|
||||
f"(leaving it for manual/Cockpit reconnect from here on)", "warn")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=wlan0 boot-reconnect backstop (NM no-secrets wedge after a boot-time supplicant glitch)
|
||||
After=NetworkManager.service
|
||||
Wants=NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/sbin/van-wlan-watchdog
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user