Add implementation plan: NetworkManager-owned WANs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
@@ -0,0 +1,395 @@
|
|||||||
|
# NetworkManager-owned WANs (deploy.sh provisioning) Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Let `deploy.sh` provision NetworkManager-owned WANs (Starlink port, optional Wi-Fi uplink, cellular modem) on any host with `ENABLE_NM_WAN=1`, so `van-failover` works on the R5C exactly as on the Pi 5, without ever flapping uplinks during a routine deploy.
|
||||||
|
|
||||||
|
**Architecture:** New host variables select the behaviour. A routine deploy installs missing packages (NetworkManager only after the AP/LAN "unmanaged" file is in place), enables NM/ModemManager, writes the cellular NM connection as a keyfile if absent, and warns when the WAN netplan file is pending. The WAN netplan change happens only through an explicit `./deploy.sh --apply-network`, which backs up `/etc/netplan`, installs the rendered file, moves Armbian's DHCP rule aside, and applies from a detached transient unit that verifies connectivity and rolls back automatically.
|
||||||
|
|
||||||
|
**Tech Stack:** bash (`set -euo pipefail`), sed/awk, systemd (`systemd-run`), NetworkManager keyfiles, netplan, apt. Tests: `tests/check-templates.sh` (plain bash, `ok`/`FAIL` lines).
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-23-nm-wan-provisioning-design.md` (builds on `2026-09-23-host-config-design.md`)
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Subagents never touch a live host, never push, and never run `./deploy.sh` without a sub-command (`--check`, `--diff`, `--templates`, `--post-check`, `--classify-unit`, `--classify-radio`, `--redact`, `--wan-plan`, `--print-apply-helper`, `render` are the safe ones; they need no root and change nothing).
|
||||||
|
- New required flag `ENABLE_NM_WAN` (exactly 0 or 1). New optional vars `CELLULAR_CONN_NAME`, `CELLULAR_APN`; `WIFI_WAN_IFACE` becomes optional (empty = no Wi-Fi uplink). Derived var `CELLULAR_UUID`.
|
||||||
|
- Guard convention (test check 6): units/paths of a flagged feature only appear inside a column-0 `if [ "$ENABLE_X" = 1 ]; then … fi` guard with 4-space-indented contents (comment and `echo` lines are exempt).
|
||||||
|
- A routine `./deploy.sh` never changes `/etc/netplan/*` or restarts the network for WAN purposes. Only `--apply-network` does.
|
||||||
|
- Rendered `50-van-wan.yaml` for `hosts/wan.conf` must stay byte-identical to what the pre-split single template rendered (the Pi 5 must see no change).
|
||||||
|
- Hosts: `hosts/van.conf` (R5C, `van`) and `hosts/wan.conf` (Pi 5, `wan`). Both get `ENABLE_NM_WAN=1`, `CELLULAR_CONN_NAME=Koodo`, `CELLULAR_APN=sp.koodo.com`. `hosts/van.conf` gets `WIFI_WAN_IFACE=` (empty); `hosts/wan.conf` keeps `WIFI_WAN_IFACE=wlan0`.
|
||||||
|
- Commit messages end with two trailer lines: `Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>` and `Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9`.
|
||||||
|
- Never put secrets in `hosts/*.conf`; the Wapana PSK stays exactly where it is (moved verbatim into the fragment).
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `deploy.sh` (modify) | vars/flags, apt auto-install, NM section, `--wan-plan`, `--apply-network`, `--print-apply-helper`, drift warning, post_check additions |
|
||||||
|
| `ap/50-van-wan.yaml` (modify) | ethernet (Starlink) part only |
|
||||||
|
| `ap/50-van-wan-wifi.yaml` (create) | the `wifis:` stanza (Wapana AP), appended only when `WIFI_WAN_IFACE` is set |
|
||||||
|
| `ap/cellular.nmconnection` (create) | NM keyfile template for the cellular connection |
|
||||||
|
| `failover/config.json` (modify) | `connection` becomes `@CELLULAR_CONN_NAME@` |
|
||||||
|
| `hosts/vanlink.conf.example`, `hosts/van.conf`, `hosts/wan.conf` (modify) | new variables |
|
||||||
|
| `tests/check-templates.sh` (modify) | new checks 14–19 |
|
||||||
|
| `README.md` (modify) | short multi-WAN paragraph |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Variables, template split, cellular keyfile template
|
||||||
|
|
||||||
|
**Files:** modify `deploy.sh` (variable arrays, `load_conf`, drift check, new `render_wan_netplan`), `ap/50-van-wan.yaml`, `failover/config.json`, `hosts/*.conf*`, `tests/check-templates.sh`; create `ap/50-van-wan-wifi.yaml`, `ap/cellular.nmconnection`.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces in `deploy.sh`: `ENABLE_NM_WAN` in `REQUIRED_VARS` and in `load_conf`'s flag loop; `WIFI_WAN_IFACE` moved from `REQUIRED_VARS` to `OPTIONAL_VARS`; `CELLULAR_CONN_NAME CELLULAR_APN` appended to `OPTIONAL_VARS`; `CELLULAR_UUID` appended to `DERIVED_VARS` and set in `load_conf`; function `render_wan_netplan` (stdout = rendered `ap/50-van-wan.yaml`, plus rendered `ap/50-van-wan-wifi.yaml` when `WIFI_WAN_IFACE` is non-empty).
|
||||||
|
- Consumed later: all of the above.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests (checks 14–16)** — insert before the final `exit "$fail"` of `tests/check-templates.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 14. the netplan split leaves the Pi 5's rendered WAN netplan byte-identical, and the R5C (no Wi-Fi WAN) omits wifis
|
||||||
|
old=$(mktemp)
|
||||||
|
git show 223652b:ap/50-van-wan.yaml > "$old"
|
||||||
|
want=$(VANLINK_CONF=hosts/wan.conf ./deploy.sh render "$old")
|
||||||
|
tmpw=$(mktemp)
|
||||||
|
# render_wan_netplan is internal; expose it through --render-wan-netplan (documented in the header)
|
||||||
|
VANLINK_CONF=hosts/wan.conf ./deploy.sh --render-wan-netplan > "$tmpw"
|
||||||
|
if [ "$want" = "$(cat "$tmpw")" ]; then ok "wan.conf: split netplan renders identical to the old single template"; else bad "wan.conf: netplan render changed by the split"; diff <(echo "$want") "$tmpw" | head -10 | sed 's/^/ /'; fi
|
||||||
|
if VANLINK_CONF=hosts/van.conf ./deploy.sh --render-wan-netplan | grep -q '^ wifis:'; then bad "van.conf: wifis stanza must be omitted (WIFI_WAN_IFACE empty)"; else ok "van.conf: no wifis stanza"; fi
|
||||||
|
rm -f "$tmpw" "$old"
|
||||||
|
|
||||||
|
# 15. cellular connection name comes from the host config
|
||||||
|
expect van failover/config.json '"connection": "Koodo"'
|
||||||
|
expect wan failover/config.json '"connection": "Koodo"'
|
||||||
|
|
||||||
|
# 16. cellular keyfile template renders a valid NM gsm profile with a stable uuid
|
||||||
|
for h in van wan; do
|
||||||
|
k=$(VANLINK_CONF=hosts/$h.conf ./deploy.sh render ap/cellular.nmconnection)
|
||||||
|
for pat in '^id=Koodo$' '^type=gsm$' '^apn=sp.koodo.com$' '^route-metric=300$' '^never-default=true$' '^uuid=[0-9a-f-]{36}$'; do
|
||||||
|
if echo "$k" | grep -qE "$pat"; then ok "$h: cellular keyfile has $pat"; else bad "$h: cellular keyfile lacks $pat"; fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
`git show 223652b:…` is the last commit before this work touched the netplan template (the spec commit); the test needs that object in history, so do not squash it away. Run: `./tests/check-templates.sh; echo exit=$?` — expected FAILs for 14–16 (no `--render-wan-netplan`, no keyfile template, no variables), `exit=1`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Split the netplan template.** In `ap/50-van-wan.yaml`, cut the whole `wifis:` block (from the line ` wifis:` through the last line of the file, including the Wapana `access-points` entry and its `password`) and paste it **verbatim** into the new file `ap/50-van-wan-wifi.yaml`, with no header comment and no blank line before ` wifis:`. Make sure `ap/50-van-wan.yaml` still ends with a newline. Then `render_wan_netplan` (Step 5) for `hosts/wan.conf` must reproduce the old single template byte for byte: check 14 proves it. (Explain the split in a comment next to `render_wan_netplan` in `deploy.sh` instead.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Cellular keyfile template** — create `ap/cellular.nmconnection` (installed 0600 by Task 2):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# NetworkManager connection for the cellular WAN (see failover/config.json, van-failover).
|
||||||
|
# Created by deploy.sh only when absent; never rewritten. No modem needs to be present.
|
||||||
|
# never-default: van-failover is the sole owner of the default route (same as the Wi-Fi/Starlink profiles).
|
||||||
|
[connection]
|
||||||
|
id=@CELLULAR_CONN_NAME@
|
||||||
|
uuid=@CELLULAR_UUID@
|
||||||
|
type=gsm
|
||||||
|
autoconnect=true
|
||||||
|
|
||||||
|
[gsm]
|
||||||
|
apn=@CELLULAR_APN@
|
||||||
|
|
||||||
|
[ipv4]
|
||||||
|
method=auto
|
||||||
|
route-metric=300
|
||||||
|
never-default=true
|
||||||
|
|
||||||
|
[ipv6]
|
||||||
|
method=auto
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: `failover/config.json`:** change `"connection": "Koodo",` to `"connection": "@CELLULAR_CONN_NAME@",`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: `deploy.sh` variables and functions.**
|
||||||
|
1. `REQUIRED_VARS`: remove `WIFI_WAN_IFACE`, add `ENABLE_NM_WAN` after `ENABLE_FRIGATE`.
|
||||||
|
2. `OPTIONAL_VARS`: add `WIFI_WAN_IFACE CELLULAR_CONN_NAME CELLULAR_APN`.
|
||||||
|
3. `DERIVED_VARS=(ONBOARD_LAN_UNMANAGED CELLULAR_UUID)`.
|
||||||
|
4. In `load_conf`'s flag loop add `ENABLE_NM_WAN`; after the `ONBOARD_LAN_UNMANAGED=` line add:
|
||||||
|
```bash
|
||||||
|
CELLULAR_UUID=$(uuidgen -s -n @dns -N "vanlink-${CELLULAR_CONN_NAME:-}" 2>/dev/null || true)
|
||||||
|
```
|
||||||
|
5. Add, next to `render()`:
|
||||||
|
```bash
|
||||||
|
render_wan_netplan() { # ap/50-van-wan.yaml, plus the wifis stanza when the host has a Wi-Fi uplink
|
||||||
|
render ap/50-van-wan.yaml
|
||||||
|
if [ -n "${WIFI_WAN_IFACE:-}" ]; then
|
||||||
|
render ap/50-van-wan-wifi.yaml
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
6. In the `case` block add: `--render-wan-netplan) load_conf "$VANLINK_CONF"; render_wan_netplan; exit 0 ;;` and document it in the header comment. Change the netplan drift check to use `render_wan_netplan` instead of `render ap/50-van-wan.yaml` (all three places, including the `diff -u` and the two warn texts).
|
||||||
|
7. Test check 2's `exempt` list gains `ap/50-van-wan-wifi.yaml` and `ap/cellular.nmconnection` (installed by dedicated code, not `install_rendered <src> /abs/dst` literal lines) — add a comment saying so.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Host files.** `hosts/vanlink.conf.example`: document `ENABLE_NM_WAN`, `CELLULAR_CONN_NAME`, `CELLULAR_APN`, and mark `WIFI_WAN_IFACE` optional (empty = none). `hosts/van.conf`: `WIFI_WAN_IFACE=` (empty, with a comment: no spare radio; the R5C's two radios are APs), `ENABLE_NM_WAN=1`, `CELLULAR_CONN_NAME=Koodo`, `CELLULAR_APN=sp.koodo.com`. `hosts/wan.conf`: same three additions, `WIFI_WAN_IFACE=wlan0` unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 6b: Fix the existing check-5 assertions that assume the R5C has `wlan0`.** With `hosts/van.conf` now `WIFI_WAN_IFACE=` (empty) these existing lines in `tests/check-templates.sh` fail and must change for `van` only (leave the `wan` ones): `expect van failover/config.json '"device": "wlan0"'` becomes `'"device": ""'`; `expect van router-status/config.json '"wifi_wan_iface": "wlan0"'` becomes `'"wifi_wan_iface": ""'`; `expect van failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0'` becomes `'ExecStart=/usr/local/sbin/van-wlan-watchdog'` (the script defaults to `wlan0` when given no argument, exactly like today's behaviour on a host without that device). Add the matching `wan` assertions if they are missing (`"device": "wlan0"`, `van-wlan-watchdog wlan0`). Controller already verified that `van-failover` handles an empty `device` safely: `resolve()` falls to the `connection` branch, `ip_iface(None)` returns `None`, and `sh()` swallows `OSError`, so the WAN simply reads "not present".
|
||||||
|
|
||||||
|
- [ ] **Step 7: Verify.** Run `./tests/check-templates.sh` (expect all ok, exit 0), `./deploy.sh --check hosts/van.conf`, `./deploy.sh --check hosts/wan.conf`, `bash -n deploy.sh`.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit** (`Template NM WAN pieces: netplan split, cellular keyfile, new host variables`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Package auto-install, NM section, `--wan-plan`
|
||||||
|
|
||||||
|
**Files:** modify `deploy.sh`, `tests/check-templates.sh`.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes (Task 1): `ENABLE_NM_WAN`, `CELLULAR_*`, `render`, `render_wan_netplan`, `load_conf`.
|
||||||
|
- Produces: `--wan-plan [conf]` (read-only, no root) printing lines `wan-plan: ENABLE_NM_WAN=<0|1>`, then when 1: ` wifi WAN: <iface>|none (wifis stanza omitted)`, ` packages: network-manager <installed|would install>`, ` netplan: /etc/netplan/50-van-wan.yaml <missing|differs|matches>`, ` armbian: /etc/netplan/10-dhcp-all-interfaces.yaml <present -> would move aside|absent>`, ` cellular profile: <name> apn=<apn> <exists|would create>` or ` cellular profile: skipped (no CELLULAR_APN)`. Functions `nm_wan_state_netplan` (echo `missing|differs|matches`) reused by Task 3.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing tests (check 17)**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 17. --wan-plan describes what would happen (config-derived lines only; no root, no changes)
|
||||||
|
for h in van wan; do
|
||||||
|
if ! p=$(VANLINK_CONF=hosts/$h.conf ./deploy.sh --wan-plan 2>&1); then bad "$h: --wan-plan failed"; echo "$p" | sed 's/^/ /'; continue; fi
|
||||||
|
echo "$p" | grep -q '^wan-plan: ENABLE_NM_WAN=1$' && ok "$h: wan-plan header" || bad "$h: wan-plan header"
|
||||||
|
echo "$p" | grep -q 'cellular profile: Koodo apn=sp.koodo.com' && ok "$h: wan-plan cellular" || bad "$h: wan-plan cellular"
|
||||||
|
done
|
||||||
|
VANLINK_CONF=hosts/van.conf ./deploy.sh --wan-plan | grep -q 'wifi WAN: none' && ok "van: wan-plan says no Wi-Fi WAN" || bad "van: wan-plan Wi-Fi WAN"
|
||||||
|
VANLINK_CONF=hosts/wan.conf ./deploy.sh --wan-plan | grep -q 'wifi WAN: wlan0' && ok "wan: wan-plan Wi-Fi WAN wlan0" || bad "wan: wan-plan Wi-Fi WAN"
|
||||||
|
```
|
||||||
|
Do NOT extend check 6's awk `need[...]` table for the NM names: `network-manager` and `.nmconnection` legitimately appear in unguarded functions and sub-command arms (`--wan-plan`, `--apply-network`), and `NetworkManager`/`nmcli` already appear in unconditional install paths and lines. The NM section itself still follows the column-0 guard convention. Run: expect FAIL for 17 (no `--wan-plan`).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Auto-install of required packages.** In `deploy.sh`, `network-manager` is NOT in `REQUIRED_PACKAGES`. Replace the loop
|
||||||
|
```bash
|
||||||
|
for pkg in "${REQUIRED_PACKAGES[@]}"; do
|
||||||
|
dpkg -s "$pkg" >/dev/null 2>&1 || { echo "MISSING REQUIRED: $pkg (apt install $pkg)"; exit 1; }
|
||||||
|
done
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```bash
|
||||||
|
missing_pkgs=()
|
||||||
|
for pkg in "${REQUIRED_PACKAGES[@]}"; do
|
||||||
|
dpkg -s "$pkg" >/dev/null 2>&1 || missing_pkgs+=("$pkg")
|
||||||
|
done
|
||||||
|
if [ ${#missing_pkgs[@]} -gt 0 ]; then
|
||||||
|
echo " installing missing required packages: ${missing_pkgs[*]}"
|
||||||
|
apt-get update -qq || echo " -> apt-get update failed (continuing with the cached lists)"
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing_pkgs[@]}" \
|
||||||
|
|| { echo "deploy: apt-get install failed for: ${missing_pkgs[*]}" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: The NM section.** Add a guarded block **after** the access-point section (so `van-ap-unmanaged.conf` is already installed) and before the `== apply ==` section:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ "$ENABLE_NM_WAN" = 1 ]; then
|
||||||
|
echo "== networkmanager WANs =="
|
||||||
|
if ! dpkg -s network-manager >/dev/null 2>&1; 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
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y network-manager \
|
||||||
|
|| { echo "deploy: apt-get install network-manager failed" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
systemctl enable --now NetworkManager ModemManager >/dev/null 2>&1 || true
|
||||||
|
nmcli general reload conf 2>/dev/null || true
|
||||||
|
for nm_if in "$WIFI_5G_IFACE" "$WIFI_2G_IFACE" "$LAN_IFACE" ${ONBOARD_LAN_IFACE:-}; do
|
||||||
|
[ -e "/sys/class/net/$nm_if" ] || continue
|
||||||
|
nm_state=$(nmcli -t -f DEVICE,STATE device 2>/dev/null | awk -F: -v d="$nm_if" '$1 == d { print $2 }') || true
|
||||||
|
[ "$nm_state" = unmanaged ] \
|
||||||
|
|| warn "NetworkManager manages $nm_if (state: ${nm_state:-unknown}) — AP/LAN ports must be unmanaged (see ap/van-ap-unmanaged.conf)"
|
||||||
|
done
|
||||||
|
if [ -n "${CELLULAR_APN:-}" ]; then
|
||||||
|
nm_keyfile="/etc/NetworkManager/system-connections/${CELLULAR_CONN_NAME}.nmconnection"
|
||||||
|
if [ -e "$nm_keyfile" ]; then
|
||||||
|
echo " cellular connection ${CELLULAR_CONN_NAME} exists — left unchanged"
|
||||||
|
else
|
||||||
|
install_rendered ap/cellular.nmconnection "$nm_keyfile" 0600
|
||||||
|
nmcli connection reload 2>/dev/null || true
|
||||||
|
echo " created cellular connection ${CELLULAR_CONN_NAME} (apn ${CELLULAR_APN}) — no modem needed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$(nm_wan_state_netplan)" != matches ]; then
|
||||||
|
warn "WAN netplan is 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
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
Requirements: inside the guard, contents indented 4 spaces; the existing drift check (`== netplan drift check ==`) must be **replaced** by this block's warning when `ENABLE_NM_WAN=1` and left as-is for hosts with the flag at 0 (wrap the old check in `if [ "$ENABLE_NM_WAN" != 1 ]; then … fi` — a `!=` guard is fine for check 6 since it contains no flagged unit names). Add to the enable list (`set_units`): when `ENABLE_NM_WAN=1`, `NetworkManager ModemManager` are appended to `ENABLE_UNITS` (keep the column-0 guard style used by `set_units`). Add near `render_wan_netplan`:
|
||||||
|
```bash
|
||||||
|
nm_wan_state_netplan() { # missing | differs | matches (the installed WAN netplan vs the rendered one)
|
||||||
|
if [ ! -f /etc/netplan/50-van-wan.yaml ]; then echo missing
|
||||||
|
elif diff -q <(render_wan_netplan) /etc/netplan/50-van-wan.yaml >/dev/null 2>&1; then echo matches
|
||||||
|
else echo differs; fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: `--wan-plan`** in the `case` block (needs `load_conf`, no root, no changes):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--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 [ -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)"
|
||||||
|
elif [ -e "/etc/NetworkManager/system-connections/${CELLULAR_CONN_NAME}.nmconnection" ]; then
|
||||||
|
echo " cellular profile: ${CELLULAR_CONN_NAME} apn=${CELLULAR_APN} exists"
|
||||||
|
else
|
||||||
|
echo " cellular profile: ${CELLULAR_CONN_NAME} apn=${CELLULAR_APN} would create"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
```
|
||||||
|
Document `--wan-plan` in the deploy.sh header comment. Because `nm_wan_state_netplan` and `render_wan_netplan` are defined before the `case` block (place both next to `render`), they are usable here.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify.** `./tests/check-templates.sh` (all ok, exit 0), `bash -n deploy.sh`, `./deploy.sh --wan-plan hosts/van.conf` and `hosts/wan.conf` (eyeball output).
|
||||||
|
- [ ] **Step 6: Commit** (`deploy: install missing packages, NetworkManager WAN section, --wan-plan`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: `--apply-network` with rollback
|
||||||
|
|
||||||
|
**Files:** modify `deploy.sh`, `tests/check-templates.sh`.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes (Tasks 1–2): `render_wan_netplan`, `nm_wan_state_netplan`, `load_conf`, `die`, `ENABLE_NM_WAN`, `STARLINK_IFACE`.
|
||||||
|
- Produces: `apply_network` (root); `apply_helper_script <backup-dir>` printing the detached helper script to stdout; sub-commands `--apply-network` (root) and `--print-apply-helper [conf]` (no root; for tests and review; uses a fake backup dir path).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing test (check 18)** — before the final `exit`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 18. the detached apply helper: syntax, ordering, rollback branch
|
||||||
|
h=$(VANLINK_CONF=hosts/van.conf ./deploy.sh --print-apply-helper 2>&1) || { bad "--print-apply-helper failed"; echo "$h" | sed 's/^/ /'; h=; }
|
||||||
|
if [ -n "$h" ]; then
|
||||||
|
echo "$h" | bash -n && ok "apply helper: bash -n" || bad "apply helper: bash -n"
|
||||||
|
echo "$h" | grep -q 'netplan generate' && echo "$h" | grep -q 'netplan apply' && ok "apply helper applies netplan" || bad "apply helper lacks netplan generate/apply"
|
||||||
|
echo "$h" | grep -q 'restoring' && ok "apply helper has a rollback branch" || bad "apply helper lacks rollback"
|
||||||
|
echo "$h" | grep -q 'enP2p33s0' && ok "apply helper waits on the Starlink port" || bad "apply helper does not reference STARLINK_IFACE"
|
||||||
|
# rollback must come after the wait loop, and the backup is restored wholesale
|
||||||
|
a=$(echo "$h" | grep -n 'netplan apply' | head -1 | cut -d: -f1); r=$(echo "$h" | grep -n 'restoring' | head -1 | cut -d: -f1)
|
||||||
|
[ "$a" -lt "$r" ] && ok "apply helper: apply precedes rollback" || bad "apply helper: rollback ordering"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
Run: expect FAIL (no sub-command).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Helper generator** (place with the other functions, before the `case` block):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apply_helper_script() { # apply_helper_script <backup-dir> — the script the detached unit runs
|
||||||
|
local bak=$1
|
||||||
|
cat <<HELPER
|
||||||
|
#!/bin/bash
|
||||||
|
# Generated by deploy.sh --apply-network: apply the WAN netplan, verify the uplink, roll back if it did not come back.
|
||||||
|
exec >>/var/log/vanlink-apply-network.log 2>&1
|
||||||
|
echo "== apply-network \$(date -Is) (backup: $bak)"
|
||||||
|
netplan generate && netplan apply
|
||||||
|
ok=0
|
||||||
|
for i in \$(seq 1 60); do
|
||||||
|
have_default=0; ip route show default | grep . >/dev/null && have_default=1
|
||||||
|
nm_ok=1
|
||||||
|
if [ -e /sys/class/net/$STARLINK_IFACE ]; then
|
||||||
|
nm_ok=0
|
||||||
|
nmcli -t -f DEVICE,STATE device 2>/dev/null | grep -Fx '$STARLINK_IFACE:connected' >/dev/null && nm_ok=1
|
||||||
|
fi
|
||||||
|
if [ \$have_default = 1 ] && [ \$nm_ok = 1 ] && ping -c1 -W2 1.1.1.1 >/dev/null 2>&1; then ok=1; break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ \$ok = 1 ]; then
|
||||||
|
echo "apply-network OK"
|
||||||
|
else
|
||||||
|
echo "apply-network FAILED — restoring $bak"
|
||||||
|
rm -f /etc/netplan/*
|
||||||
|
cp -a $bak/. /etc/netplan/
|
||||||
|
netplan generate && netplan apply
|
||||||
|
echo "apply-network ROLLED BACK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
HELPER
|
||||||
|
}
|
||||||
|
```
|
||||||
|
(Unquoted heredoc on purpose: `$bak` and `$STARLINK_IFACE` expand now; `\$…` stay for run time. The first `netplan apply` failing must also reach the rollback: change the first `netplan generate && netplan apply` line to `netplan generate && netplan apply || echo "netplan apply failed"` so the loop still runs and rollback triggers on failure of the connectivity gate.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: `apply_network` and the sub-commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apply_network() { # ./deploy.sh --apply-network — explicit, root; see spec §4
|
||||||
|
[ "$(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 -s network-manager >/dev/null 2>&1 || die "network-manager is not installed — run ./deploy.sh first"
|
||||||
|
local stamp bak tmp
|
||||||
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
bak=/var/backups/vanlink-netplan-$stamp
|
||||||
|
install -d -m0700 "$bak"
|
||||||
|
cp -a /etc/netplan/. "$bak/"
|
||||||
|
tmp=$(mktemp)
|
||||||
|
render_wan_netplan > "$tmp"
|
||||||
|
check_rendered ap/50-van-wan.yaml "$tmp" || { rm -f "$tmp"; exit 1; }
|
||||||
|
install -m0600 "$tmp" /etc/netplan/50-van-wan.yaml
|
||||||
|
rm -f "$tmp"
|
||||||
|
if [ -f /etc/netplan/10-dhcp-all-interfaces.yaml ]; then
|
||||||
|
mv /etc/netplan/10-dhcp-all-interfaces.yaml /etc/netplan/10-dhcp-all-interfaces.yaml.vanlink-disabled
|
||||||
|
fi
|
||||||
|
: > /var/log/vanlink-apply-network.log
|
||||||
|
apply_helper_script "$bak" > /run/vanlink-apply-network.sh
|
||||||
|
chmod 0700 /run/vanlink-apply-network.sh
|
||||||
|
echo "backup: $bak log: /var/log/vanlink-apply-network.log"
|
||||||
|
echo "applying from a detached unit (survives an SSH drop); waiting up to 90 s ..."
|
||||||
|
systemctl reset-failed vanlink-apply-network 2>/dev/null || true
|
||||||
|
systemd-run --unit=vanlink-apply-network --collect /bin/bash /run/vanlink-apply-network.sh >/dev/null
|
||||||
|
local t=0
|
||||||
|
while systemctl is-active --quiet vanlink-apply-network && [ "$t" -lt 90 ]; do sleep 1; t=$((t + 1)); done
|
||||||
|
tail -n 6 /var/log/vanlink-apply-network.log 2>/dev/null || true
|
||||||
|
grep -q 'apply-network OK' /var/log/vanlink-apply-network.log 2>/dev/null \
|
||||||
|
|| die "apply-network did not report OK — see /var/log/vanlink-apply-network.log (previous config: $bak)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
In the `case` block add (documented in the header):
|
||||||
|
```bash
|
||||||
|
--apply-network)
|
||||||
|
load_conf "$VANLINK_CONF"
|
||||||
|
apply_network
|
||||||
|
exit 0 ;;
|
||||||
|
--print-apply-helper) # for review/tests: the helper script, with a placeholder backup dir; no root, no changes
|
||||||
|
load_conf "${2:-$VANLINK_CONF}"
|
||||||
|
apply_helper_script /var/backups/vanlink-netplan-EXAMPLE
|
||||||
|
exit 0 ;;
|
||||||
|
```
|
||||||
|
The log is truncated at the start of each run (the `: >` line above), so the `apply-network OK` grep only ever sees the current run.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify.** `./tests/check-templates.sh` (all ok, exit 0), `bash -n deploy.sh`, `./deploy.sh --print-apply-helper hosts/van.conf | bash -n`. Read the printed helper once and confirm: rollback removes `/etc/netplan/*` then restores the backup wholesale (so the moved-aside Armbian file returns), exit status 1 on failure.
|
||||||
|
- [ ] **Step 5: Commit** (`deploy: --apply-network with detached apply, connectivity gate and automatic rollback`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Post-check additions and docs
|
||||||
|
|
||||||
|
**Files:** modify `deploy.sh` (`post_check`), `tests/check-templates.sh`, `README.md`.
|
||||||
|
|
||||||
|
**Interfaces:** consumes `post_check` structure, `warn`, `retry`/shared deadline, `ENABLE_NM_WAN`, `CELLULAR_*`, `LAN/AP` iface vars, `/run/van-failover/state.json`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing test (check 19)**: with a temp conf outside the repo built from `hosts/van.conf` with `ENABLE_NM_WAN=0` forced (same pattern as check 11(ii)), `POSTCHECK_WAIT=0 ./deploy.sh --post-check "$tmpconf"` output must not contain `NetworkManager`; with `hosts/van.conf` as is, `POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf` output must mention `NetworkManager` (as a unit check or a warning — on the laptop it will be a warning or an ok line, either way present).
|
||||||
|
- [ ] **Step 2: `post_check` additions**, all inside `if [ "$ENABLE_NM_WAN" = 1 ]; then … fi` with indented contents, each probe guarded for `set -euo pipefail`:
|
||||||
|
1. NetworkManager/ModemManager: already covered by check (a) because Task 2 appended them to `ENABLE_UNITS`.
|
||||||
|
2. For each of `WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE ONBOARD_LAN_IFACE` that exists under `/sys/class/net`: NM state must be `unmanaged` (`nmcli -t -f DEVICE,STATE device`), else `warn "post-deploy: NetworkManager manages <if> …"`; ok line otherwise.
|
||||||
|
3. If `CELLULAR_APN` is set: the connection `$CELLULAR_CONN_NAME` exists (`nmcli -t -f NAME connection show`, or its keyfile), else warn.
|
||||||
|
4. `/run/van-failover/state.json`: via a guarded `python3 -c` (no eval, no jq dependency), for each WAN whose `device` is non-empty and exists in `/sys/class/net`, `present` must be true; warn per WAN where it is false; warn once if the file is absent.
|
||||||
|
5. Netplan: `nm_wan_state_netplan` must be `matches`, else warn `post-deploy: WAN netplan <state> — run ./deploy.sh --apply-network` (the routine-deploy warning is repeated here so `--post-check` shows it too).
|
||||||
|
- [ ] **Step 3: README.** Add a short section "Multi-WAN (NetworkManager)" under the header note: which hosts use it (`ENABLE_NM_WAN`), that `deploy.sh` installs the packages and the cellular NM connection, that the WAN netplan is applied only by `./deploy.sh --apply-network` (with automatic rollback and log `/var/log/vanlink-apply-network.log`), and how to read `/run/van-failover/state.json`.
|
||||||
|
- [ ] **Step 4: Verify.** `./tests/check-templates.sh` (exit 0, no FAIL), `bash -n deploy.sh`, `POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf` (runs without abort on the laptop).
|
||||||
|
- [ ] **Step 5: Commit** (`post-check and docs for NetworkManager WANs`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Roll out to the R5C — controller-run, needs the user's go-ahead
|
||||||
|
|
||||||
|
Not delegated. Preconditions to confirm with the user: console or ZeroTier access path (192.168.196.43 survives only if the uplink survives), the test dongle `wlx200bd04dc756` set as `WIFI_WAN_IFACE` in the R5C's local `hosts/van.conf` (uncommitted edit, like the 2.4GHz temp dongle), and that home Wi-Fi ("Wapana") is in range if the Wi-Fi WAN is to be tested.
|
||||||
|
Steps: `rsync` the checkout to the R5C excluding `hosts/van.conf` → `./deploy.sh --check` → `./deploy.sh --diff` (review; secrets are redacted) → `./deploy.sh --wan-plan` → routine `sudo ./deploy.sh` (installs network-manager, keyfile, warns about pending netplan) → `sudo ./deploy.sh --apply-network` → `cat /var/log/vanlink-apply-network.log`, `nmcli device`, `nmcli connection show`, `cat /run/van-failover/state.json`, `./deploy.sh --post-check`. Roll back path if it fails and the automatic one did not: restore `/var/backups/vanlink-netplan-*`.
|
||||||
|
|
||||||
|
### Task 6: Pi 5 — routine deploy only, needs the user's go-ahead
|
||||||
|
|
||||||
|
Preconditions: the open questions from the host-config rollout (which of Frigate/Cockpit/Li3/nvme-watch are live; maintenance window). `./deploy.sh --diff` must show `50-van-wan.yaml` unchanged; `./deploy.sh --wan-plan` must say `netplan: … matches`. `--apply-network` is **not** run on the Pi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review (done)
|
||||||
|
|
||||||
|
**Spec coverage:** variables (Task 1); package auto-install incl. NM ordering and unmanaged verification (Task 2); routine-deploy behaviour incl. cellular keyfile, no netplan changes, warning text (Task 2); `--apply-network` steps 1–6 incl. backup, Armbian file moved aside, detached unit, connectivity gate, rollback, log (Task 3); template split with byte-identical Pi render and omitted wifis on the R5C (Task 1, check 14); post-check additions (Task 4); tests incl. `--wan-plan`, flag guards, rollback ordering (checks 14–19); rollout order (Tasks 5–6). Deviation from the spec text, deliberate: the cellular profile is written as a NetworkManager keyfile from a template instead of via `nmcli connection add` (deterministic, reviewable, no nmcli quirks for interface-less gsm profiles).
|
||||||
|
**Placeholders:** none. **Names:** `ENABLE_NM_WAN`, `CELLULAR_CONN_NAME`, `CELLULAR_APN`, `CELLULAR_UUID`, `render_wan_netplan`, `nm_wan_state_netplan`, `apply_helper_script`, `apply_network`, sub-commands `--render-wan-netplan --wan-plan --apply-network --print-apply-helper` are used consistently across tasks and tests.
|
||||||
Reference in New Issue
Block a user