1025 lines
46 KiB
Markdown
1025 lines
46 KiB
Markdown
# Host-local Configuration (`/etc/vanlink.conf`) 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:** Replace the tracked, hand-edited `deploy.conf` with one versioned file per host under `hosts/`, reached through an `/etc/vanlink.conf` symlink, so the Pi 5 and the R5C share one git checkout and cannot drift.
|
||
|
||
**Architecture:** `deploy.sh` sources `/etc/vanlink.conf`, validates it, and renders `@TOKEN@` placeholders in every template (generic loop over a variable list). `install_rendered` refuses output that still contains a token. Optional services are gated by `ENABLE_*` flags. A plain-bash test script (`tests/check-templates.sh`) statically enforces the rules that were broken on 2026-09-23.
|
||
|
||
**Tech Stack:** bash (`set -euo pipefail`), sed, awk, systemd units, networkd/NetworkManager/hostapd/dnsmasq/nftables configs. No test framework: `tests/check-templates.sh` prints `ok`/`FAIL` lines and exits non-zero on failure.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-09-23-host-config-design.md`
|
||
|
||
## Global Constraints
|
||
|
||
- Host config path is `/etc/vanlink.conf` (override with env `VANLINK_CONF`); host files are `hosts/<hostname -s>.conf`: `hosts/van.conf` (R5C, hostname `van`) and `hosts/wan.conf` (Pi 5, hostname `wan`).
|
||
- Host files contain **no secrets** (they are in git and read by root).
|
||
- Required variables: `WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE WIFI_WAN_IFACE GATEWAY_IP GATEWAY_PREFIX GATEWAY_NETWORK DHCP_RANGE_START DHCP_RANGE_END DNS_RESOLVERS MODEM_USB_VENDOR ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE`.
|
||
- Optional variables: `ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID`. `NVME_DEVICE` and `NVME_FS_DEVICE` are required only when `ENABLE_NVME_WATCH=1`.
|
||
- `ENABLE_*` flags are exactly `0` or `1`.
|
||
- `LAN_USB_IFACE` is renamed `LAN_IFACE` everywhere; the JSON key `lan_usb_iface` in `router-status/config.json` is **not** renamed (consumer-facing).
|
||
- `deploy.sh` never creates or edits `/etc/vanlink.conf` or `hosts/*.conf`.
|
||
- `deploy.sh` `--check`, `--templates`, `--diff` and `render` need no root and install nothing.
|
||
- LAN bridge file installs as `/etc/systemd/network/05-van-lan.network` (sorts before netplan's `10-` file).
|
||
- Commit messages end with `Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>`.
|
||
- Do not touch the `.bak` files, `ap/50-van-wan.yaml`'s PSK, or `power/thermal-config.json` (out of scope per spec).
|
||
|
||
## File Structure
|
||
|
||
| File | Responsibility |
|
||
|---|---|
|
||
| `deploy.sh` (modify) | load/validate host conf, render, install, flag gating, `--check/--templates/--diff` |
|
||
| `hosts/vanlink.conf.example` (create) | documented reference; defines exactly the variables `deploy.sh` knows |
|
||
| `hosts/van.conf`, `hosts/wan.conf` (create) | real values for the two hosts |
|
||
| `deploy.conf` (delete) | replaced by the above |
|
||
| `tests/check-templates.sh` (create) | static checks, run from anywhere, no root |
|
||
| `ap/*`, `failover/*`, `router-status/config.json`, `power/nvme-watch-config.json` (modify) | hardcoded host values become `@TOKEN@`s |
|
||
| `failover/sync-usb-backup.sh`, `failover/sync-sd-backup.sh` (modify) | read backup settings from the host conf |
|
||
| `README.md`, misc comments (modify) | describe the shared model |
|
||
|
||
---
|
||
|
||
### Task 1: Config core, host files, test harness
|
||
|
||
**Files:**
|
||
- Modify: `deploy.sh` (top section, lines 1-47)
|
||
- Create: `tests/check-templates.sh`, `hosts/vanlink.conf.example`, `hosts/van.conf`, `hosts/wan.conf`
|
||
- Delete: `deploy.conf`
|
||
- Modify (rename token): every file containing `LAN_USB_IFACE` except `.bak` and `docs/`
|
||
|
||
**Interfaces:**
|
||
- Produces (used by all later tasks), in `deploy.sh`: arrays `REQUIRED_VARS`, `OPTIONAL_VARS`, `DERIVED_VARS`, `TOKEN_VARS`; string `TOKEN_RE`; functions `die`, `load_conf <file>`, `render <file>`, `check_rendered <src> <rendered>`, `install_rendered <src> <dst> [mode]`, `template_files`; sub-commands `--check [conf]`, `--templates`, `render <file>`. Derived var `ONBOARD_LAN_UNMANAGED` (`;interface-name:<iface>` or empty).
|
||
- Produces in `tests/check-templates.sh`: helper functions `ok <msg>` / `bad <msg>` and variable `fail`; later tasks append numbered check sections before the final `exit "$fail"`.
|
||
|
||
- [ ] **Step 1: Commit the pending `deploy.sh` fix**
|
||
|
||
The working tree has an uncommitted fix from 2026-09-23 (dnsmasq and br0 rendered; LAN file installed as `05-`).
|
||
|
||
Run: `git diff --stat` — expected: only `deploy.sh`, ~8 insertions / 3 deletions.
|
||
|
||
```bash
|
||
git add deploy.sh
|
||
git commit -m "deploy: render dnsmasq/br0 configs, install LAN file as 05-van-lan
|
||
|
||
van-ap-dnsmasq.conf and 21-van-br0.network carry @TOKEN@ placeholders but
|
||
were installed unrendered. Armbian's netplan-generated 10-netplan-* file
|
||
matched the LAN port before 22-van-lan.network, so it was never bridged.
|
||
|
||
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
- [ ] **Step 2: Write the test script (checks 1, 3, 4) and see it fail**
|
||
|
||
Create `tests/check-templates.sh` (mode 0755):
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Static checks for the host-config templating. No root needed.
|
||
# ./tests/check-templates.sh
|
||
set -euo pipefail
|
||
cd "$(dirname "$(readlink -f "$0")")/.."
|
||
|
||
fail=0
|
||
ok() { echo "ok - $*"; }
|
||
bad() { echo "FAIL - $*"; fail=1; }
|
||
|
||
# 1. shell syntax
|
||
for f in deploy.sh failover/sync-usb-backup.sh failover/sync-sd-backup.sh tests/check-templates.sh; do
|
||
if bash -n "$f"; then ok "bash -n $f"; else bad "bash -n $f"; fi
|
||
done
|
||
|
||
# 3. every host file passes deploy.sh --check (validates vars, renders all templates)
|
||
for c in hosts/*.conf; do
|
||
if out=$(./deploy.sh --check "$c" 2>&1); then
|
||
ok "--check $c"
|
||
else
|
||
bad "--check $c"; echo "$out" | sed 's/^/ /'
|
||
fi
|
||
done
|
||
|
||
# 4. the example defines exactly the variables deploy.sh knows about
|
||
known=$(sed -n '/^REQUIRED_VARS=(/,/^)/p;/^OPTIONAL_VARS=(/,/^)/p' deploy.sh \
|
||
| grep -vE '^[A-Z_]+=\(|^\)' | tr -s ' \t' '\n' | grep -v '^$' | sort || true)
|
||
defined=$(grep -oE '^[A-Z][A-Z0-9_]*=' hosts/vanlink.conf.example 2>/dev/null | tr -d = | sort || true)
|
||
if [ -n "$known" ] && [ "$known" = "$defined" ]; then
|
||
ok "hosts/vanlink.conf.example defines exactly the variables deploy.sh knows"
|
||
else
|
||
bad "hosts/vanlink.conf.example differs from deploy.sh's variable lists"
|
||
diff <(echo "$known") <(echo "$defined") | sed 's/^/ /' || true
|
||
fi
|
||
|
||
exit "$fail"
|
||
```
|
||
|
||
Run: `chmod +x tests/check-templates.sh && ./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: FAIL lines for `--check hosts/*.conf` (glob matches nothing / no `--check`) and for the example; `exit=1`.
|
||
|
||
- [ ] **Step 3: Create the host files**
|
||
|
||
`hosts/vanlink.conf.example`:
|
||
|
||
```bash
|
||
# hosts/vanlink.conf.example — reference for /etc/vanlink.conf.
|
||
#
|
||
# Each host has hosts/<hostname>.conf (hostname -s) in this repo, and
|
||
# /etc/vanlink.conf is a symlink to it:
|
||
# sudo ln -sfn "$PWD/hosts/$(hostname -s).conf" /etc/vanlink.conf
|
||
# Plain KEY=value (it is sourced by bash). NO SECRETS — this file is in git.
|
||
# Validate without root: ./deploy.sh --check hosts/<hostname>.conf
|
||
# Find interface names with `ip -br link` (wired) / `iw dev` (wifi); USB IDs with lsusb.
|
||
|
||
# --- Interfaces -------------------------------------------------------------
|
||
WIFI_5G_IFACE=wlp1s0 # 5GHz AP radio (hostapd)
|
||
WIFI_2G_IFACE=wlx000000000000 # 2.4GHz AP radio (hostapd-2g)
|
||
LAN_IFACE=enp1s0 # wired LAN port, bridged into br0
|
||
STARLINK_IFACE=enp2s0 # Starlink uplink (NetworkManager WAN)
|
||
WIFI_WAN_IFACE=wlan0 # Wi-Fi uplink (NetworkManager WAN)
|
||
ONBOARD_LAN_IFACE= # optional: extra onboard LAN port bridged into br0 (e.g. eth0); empty = none
|
||
|
||
# --- Addressing ---------------------------------------------------------------
|
||
GATEWAY_IP="10.42.9.1"
|
||
GATEWAY_PREFIX="24"
|
||
GATEWAY_NETWORK="10.42.9.0/24"
|
||
DHCP_RANGE_START="10.42.9.10"
|
||
DHCP_RANGE_END="10.42.9.254"
|
||
DNS_RESOLVERS="1.1.1.1 8.8.8.8"
|
||
|
||
# --- Hardware -----------------------------------------------------------------
|
||
MODEM_USB_VENDOR=2c7c # cellular modem USB vendor id
|
||
NVME_DEVICE= # only when ENABLE_NVME_WATCH=1, e.g. nvme0
|
||
NVME_FS_DEVICE= # only when ENABLE_NVME_WATCH=1, e.g. /dev/nvme0n1p2
|
||
|
||
# --- Backup (read by failover/sync-*-backup.sh; leave empty if unused) ----------
|
||
USB_BACKUP_ROOT_PART= # sync-usb-backup.sh: partition to mount, e.g. /dev/sda2
|
||
USB_BACKUP_MOUNT= # sync-usb-backup.sh: local mount point, or host:/path for NFS
|
||
SD_BACKUP_UUID= # sync-sd-backup.sh: filesystem UUID of the backup SD card
|
||
|
||
# --- Feature flags (0 or 1) ---------------------------------------------------
|
||
ENABLE_COCKPIT=0 # Cockpit "Van Router" plugin
|
||
ENABLE_LI3=0 # Li3 house-battery BLE -> MQTT service
|
||
ENABLE_NVME_WATCH=0 # NVMe timeout watchdog (needs an NVMe drive)
|
||
ENABLE_HA=0 # Home Assistant + ESPHome (podman quadlets)
|
||
ENABLE_FRIGATE=0 # Frigate NVR (podman quadlet)
|
||
```
|
||
|
||
`hosts/van.conf` (R5C; values from its current, interface-swapped `deploy.conf`, addressing and flags from what runs there today):
|
||
|
||
```bash
|
||
# hosts/van.conf — FriendlyElec NanoPi R5C, hostname "van" (Armbian).
|
||
# /etc/vanlink.conf -> this file. See hosts/vanlink.conf.example for the variable list.
|
||
|
||
WIFI_5G_IFACE=wlp1s0 # MediaTek MT7925E (PCIe); also carries the 2.4GHz AP
|
||
WIFI_2G_IFACE=wlx3c3332002066
|
||
LAN_IFACE=enP1p17s0
|
||
STARLINK_IFACE=enP2p33s0
|
||
WIFI_WAN_IFACE=wlan0 # not present on this host (kept for the failover config)
|
||
ONBOARD_LAN_IFACE=
|
||
|
||
GATEWAY_IP="10.42.1.1"
|
||
GATEWAY_PREFIX="24"
|
||
GATEWAY_NETWORK="10.42.1.0/24"
|
||
DHCP_RANGE_START="10.42.1.10"
|
||
DHCP_RANGE_END="10.42.1.254"
|
||
DNS_RESOLVERS="1.1.1.1 8.8.8.8"
|
||
|
||
MODEM_USB_VENDOR=2c7c
|
||
NVME_DEVICE=
|
||
NVME_FS_DEVICE=
|
||
|
||
USB_BACKUP_ROOT_PART=
|
||
USB_BACKUP_MOUNT=
|
||
SD_BACKUP_UUID=39118305-4927-47a4-acc7-ba022f37af22
|
||
|
||
ENABLE_COCKPIT=0
|
||
ENABLE_LI3=1
|
||
ENABLE_NVME_WATCH=0 # no NVMe on this board
|
||
ENABLE_HA=1
|
||
ENABLE_FRIGATE=1
|
||
```
|
||
|
||
`hosts/wan.conf` (Pi 5; values from its uncommitted `deploy.conf` and live state):
|
||
|
||
```bash
|
||
# hosts/wan.conf — Raspberry Pi 5, hostname "wan".
|
||
# /etc/vanlink.conf -> this file. See hosts/vanlink.conf.example for the variable list.
|
||
|
||
WIFI_5G_IFACE=wlxc83a35a4ee55 # Realtek RTL8852BU, rtw89
|
||
WIFI_2G_IFACE=wlxf4f26d1760a7 # D-Link DWA-171, RTL8821CU
|
||
LAN_IFACE=enx00e04c331140
|
||
STARLINK_IFACE=enx3c8cf861752b # TrendNet USB dongle
|
||
WIFI_WAN_IFACE=wlan0
|
||
ONBOARD_LAN_IFACE=eth0
|
||
|
||
GATEWAY_IP="10.42.0.1"
|
||
GATEWAY_PREFIX="24"
|
||
GATEWAY_NETWORK="10.42.0.0/24"
|
||
DHCP_RANGE_START="10.42.0.10"
|
||
DHCP_RANGE_END="10.42.0.254"
|
||
DNS_RESOLVERS="1.1.1.1 8.8.8.8"
|
||
|
||
MODEM_USB_VENDOR=2c7c
|
||
NVME_DEVICE=
|
||
NVME_FS_DEVICE=
|
||
|
||
USB_BACKUP_ROOT_PART=/dev/sda2
|
||
USB_BACKUP_MOUNT=10.42.0.44:/media/andreas/writable-nvme
|
||
SD_BACKUP_UUID=
|
||
|
||
ENABLE_COCKPIT=0
|
||
ENABLE_LI3=0
|
||
ENABLE_NVME_WATCH=0 # boots from mmcblk0; no NVMe fitted
|
||
ENABLE_HA=1
|
||
ENABLE_FRIGATE=0
|
||
```
|
||
|
||
- [ ] **Step 4: Replace the `deploy.conf` block at the top of `deploy.sh`**
|
||
|
||
In `deploy.sh`, replace everything from the line `# deploy.conf holds hardware-instance identifiers` through the closing `fi` of the `render` sub-command (currently lines 11-45) with:
|
||
|
||
```bash
|
||
# Host-specific values (interface names, addressing, feature flags) live in
|
||
# /etc/vanlink.conf — a symlink to hosts/<hostname>.conf in this repo. Config
|
||
# files below carry @TOKEN@ placeholders substituted from those variables via
|
||
# render()/install_rendered() — edit hosts/<hostname>.conf, not the configs.
|
||
VANLINK_CONF=${VANLINK_CONF:-/etc/vanlink.conf}
|
||
|
||
# Must be defined (non-empty) in the host file; deploy aborts naming any missing.
|
||
REQUIRED_VARS=(
|
||
WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE WIFI_WAN_IFACE
|
||
GATEWAY_IP GATEWAY_PREFIX GATEWAY_NETWORK DHCP_RANGE_START DHCP_RANGE_END
|
||
DNS_RESOLVERS MODEM_USB_VENDOR
|
||
ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE
|
||
)
|
||
# May be empty or absent; NVME_* are required only when ENABLE_NVME_WATCH=1.
|
||
OPTIONAL_VARS=(
|
||
ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE
|
||
USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID
|
||
)
|
||
# Computed in load_conf; usable as @TOKEN@ like the rest.
|
||
DERIVED_VARS=(ONBOARD_LAN_UNMANAGED)
|
||
TOKEN_VARS=("${REQUIRED_VARS[@]}" "${OPTIONAL_VARS[@]}" "${DERIVED_VARS[@]}")
|
||
TOKEN_RE='@[A-Z][A-Z0-9_]*@'
|
||
|
||
die() { echo "deploy: $*" >&2; exit 1; }
|
||
|
||
load_conf() { # load_conf <file> — source and validate a host config
|
||
local f=$1 v flag missing=()
|
||
[ -e "$f" ] || die "$f not found (missing or dangling symlink). Create it for this host with:
|
||
sudo ln -sfn \"$PWD/hosts/$(hostname -s).conf\" /etc/vanlink.conf"
|
||
for v in "${TOKEN_VARS[@]}"; do unset "$v"; done
|
||
# shellcheck disable=SC1090
|
||
. "$f"
|
||
for v in "${REQUIRED_VARS[@]}"; do
|
||
[ -n "${!v:-}" ] || missing+=("$v")
|
||
done
|
||
[ ${#missing[@]} -eq 0 ] || die "$f is missing required variable(s): ${missing[*]}"
|
||
for flag in ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE; do
|
||
case ${!flag} in
|
||
0|1) ;;
|
||
*) die "$f: $flag must be 0 or 1 (got '${!flag}')" ;;
|
||
esac
|
||
done
|
||
if [ "$ENABLE_NVME_WATCH" = 1 ]; then
|
||
{ [ -n "${NVME_DEVICE:-}" ] && [ -n "${NVME_FS_DEVICE:-}" ]; } \
|
||
|| die "$f: ENABLE_NVME_WATCH=1 needs NVME_DEVICE and NVME_FS_DEVICE"
|
||
fi
|
||
ONBOARD_LAN_UNMANAGED=${ONBOARD_LAN_IFACE:+;interface-name:$ONBOARD_LAN_IFACE}
|
||
}
|
||
|
||
render() { # render <file> -> stdout, with @TOKEN@ placeholders substituted
|
||
local args=() v
|
||
for v in "${TOKEN_VARS[@]}"; do
|
||
args+=(-e "s|@${v}@|${!v-}|g")
|
||
done
|
||
sed "${args[@]}" "$1"
|
||
}
|
||
|
||
check_rendered() { # check_rendered <src> <rendered-file> — fail on a leftover @TOKEN@
|
||
local left
|
||
left=$(grep -oE "$TOKEN_RE" "$2" | sort -u | tr '\n' ' ') || true
|
||
[ -z "$left" ] && return 0
|
||
echo "deploy: $1: unrendered token(s) after render: $left" >&2
|
||
return 1
|
||
}
|
||
|
||
install_rendered() { # install_rendered <src> <dst> [mode]
|
||
local tmp
|
||
tmp=$(mktemp)
|
||
render "$1" > "$tmp"
|
||
check_rendered "$1" "$tmp" || { rm -f "$tmp"; exit 1; }
|
||
install -D -m"${3:-0644}" "$tmp" "$2"
|
||
rm -f "$tmp"
|
||
}
|
||
|
||
template_files() { # every repo file that carries an @TOKEN@ placeholder
|
||
grep -rlE "$TOKEN_RE" --exclude-dir=.git --exclude-dir=__pycache__ \
|
||
--exclude-dir=docs --exclude-dir=tests --exclude-dir=hosts \
|
||
--exclude='*.bak' --exclude='*.md' --exclude=deploy.sh . | sed 's|^\./||' | sort
|
||
}
|
||
|
||
case "${1:-}" in
|
||
--templates)
|
||
template_files
|
||
exit 0 ;;
|
||
--check) # ./deploy.sh --check [conf] — validate conf, render every template, install nothing
|
||
load_conf "${2:-$VANLINK_CONF}"
|
||
rc=0
|
||
tmp=$(mktemp)
|
||
for f in $(template_files); do
|
||
render "$f" > "$tmp"
|
||
check_rendered "$f" "$tmp" || rc=1
|
||
done
|
||
rm -f "$tmp"
|
||
[ "$rc" -eq 0 ] && echo "check OK: $(template_files | wc -l) templates render cleanly"
|
||
exit "$rc" ;;
|
||
render)
|
||
[ -n "${2:-}" ] || { echo "Usage: $0 render <file>"; exit 1; }
|
||
load_conf "$VANLINK_CONF"
|
||
render "$2"
|
||
exit 0 ;;
|
||
esac
|
||
```
|
||
|
||
Then, directly after the existing line `[ "$(id -u)" = 0 ] || { echo "Run with sudo (writes to /etc, /usr)."; exit 1; }`, add:
|
||
|
||
```bash
|
||
load_conf "$VANLINK_CONF"
|
||
echo "== host config: $(readlink -f "$VANLINK_CONF") =="
|
||
```
|
||
|
||
- [ ] **Step 5: Rename `LAN_USB_IFACE` to `LAN_IFACE` in the remaining files and delete `deploy.conf`**
|
||
|
||
```bash
|
||
grep -rlI 'LAN_USB_IFACE' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . | grep -v '^./deploy.conf$'
|
||
```
|
||
Expected: `ap/22-van-lan.network`, `ap/van-ap-unmanaged.conf`, `router-status/config.json`, `cockpit/vanrouter/vanrouter.js` (and possibly others). Rename in exactly those files:
|
||
|
||
```bash
|
||
grep -rlI 'LAN_USB_IFACE' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . \
|
||
| grep -v '^./deploy.conf$' | xargs sed -i 's/LAN_USB_IFACE/LAN_IFACE/g'
|
||
git rm -q deploy.conf
|
||
grep -rnI 'LAN_USB_IFACE' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . || echo "none left"
|
||
```
|
||
Expected last line: `none left`. (The lowercase JSON key `lan_usb_iface` is untouched by design.)
|
||
|
||
- [ ] **Step 6: Run the test and fix any leftover-token findings**
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: all `ok`, `exit=0`. If `--check` reports `unrendered token(s)` for a file, that file has an `@…@` string that is not a defined token: either it is a typo (fix it) or legitimate text (rare; then change `template_files`' exclude list and note why in a comment).
|
||
|
||
Also run: `./deploy.sh --check hosts/van.conf && VANLINK_CONF=hosts/wan.conf ./deploy.sh render ap/van-ap-dnsmasq.conf | grep dhcp-range`
|
||
Expected: `check OK: N templates render cleanly` then `dhcp-range=10.42.0.10,10.42.0.254,255.255.255.0,12h`.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add deploy.sh tests hosts
|
||
git add -u
|
||
git commit -m "deploy: host config from /etc/vanlink.conf (hosts/<hostname>.conf)
|
||
|
||
deploy.conf is replaced by one versioned file per host plus an example.
|
||
deploy.sh validates the config, renders every template with a generic
|
||
token loop, refuses to install output with leftover @TOKEN@s and gains
|
||
--check/--templates. LAN_USB_IFACE is renamed LAN_IFACE.
|
||
|
||
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Template every remaining hardcoded host value
|
||
|
||
**Files:**
|
||
- Modify: `ap/nftables.conf`, `ap/van-br0-setup.service`, `failover/config.json`, `router-status/config.json`, `ap/50-van-wan.yaml`, `failover/van-wlan-watchdog.service`, `ap/van-ap-unmanaged.conf`, `ap/23-van-lan-eth0.network`, `power/nvme-watch-config.json`, `deploy.sh` (install lines)
|
||
- Modify: `tests/check-templates.sh` (add checks 2 and 5)
|
||
|
||
**Interfaces:**
|
||
- Consumes (Task 1): `./deploy.sh --templates`, `--check`, `render`, token vars `WIFI_WAN_IFACE`, `ONBOARD_LAN_IFACE`, `ONBOARD_LAN_UNMANAGED`, `NVME_DEVICE`, `NVME_FS_DEVICE`, `GATEWAY_NETWORK`, `LAN_IFACE`, `WIFI_5G_IFACE`.
|
||
- Produces: `tests/check-templates.sh` checks 2 (every templated file installed via `install_rendered`) and 5 (rendered host-specific content).
|
||
|
||
- [ ] **Step 1: Add checks 2 and 5 to the test script**
|
||
|
||
Insert before the final `exit "$fail"`:
|
||
|
||
```bash
|
||
# 2. every file carrying an @TOKEN@ is installed through install_rendered
|
||
# (plain `install` would ship the placeholder unrendered — the 2026-09-23 failure)
|
||
exempt=" cockpit/vanrouter/vanrouter.js " # installed by the loop in the cockpit block
|
||
for f in $(./deploy.sh --templates); do
|
||
case "$exempt" in *" $f "*) continue ;; esac
|
||
if grep -qE "^[[:space:]]*install_rendered +$f[[:space:]]" deploy.sh; then
|
||
ok "install_rendered $f"
|
||
else
|
||
bad "$f contains an @TOKEN@ but deploy.sh does not install it with install_rendered"
|
||
fi
|
||
done
|
||
|
||
# 5. host-specific values land in the rendered output
|
||
r() { VANLINK_CONF="hosts/$1.conf" ./deploy.sh render "$2"; }
|
||
expect() { # expect <host> <file> <fixed string that must appear>
|
||
if r "$1" "$2" | grep -qF -- "$3"; then ok "$1: $2 has '$3'"; else bad "$1: $2 lacks '$3'"; fi
|
||
}
|
||
expect van ap/nftables.conf 'ip saddr 10.42.1.0/24'
|
||
expect wan ap/nftables.conf 'ip saddr 10.42.0.0/24'
|
||
expect van ap/van-br0-setup.service 'brctl addif br0 enP1p17s0'
|
||
expect wan ap/van-br0-setup.service 'brctl addif br0 enx00e04c331140'
|
||
expect van failover/config.json '"device": "wlan0"'
|
||
expect van router-status/config.json '"wifi_wan_iface": "wlan0"'
|
||
expect van failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0'
|
||
expect van ap/van-ap-unmanaged.conf 'interface-name:enP1p17s0'
|
||
expect wan ap/van-ap-unmanaged.conf 'interface-name:enx00e04c331140;interface-name:eth0'
|
||
expect wan ap/23-van-lan-eth0.network 'Name=eth0'
|
||
expect van power/nvme-watch-config.json '"nvme_device": ""'
|
||
if r van ap/van-ap-unmanaged.conf | grep -q 'eth0'; then
|
||
bad "van: unmanaged-devices must not mention eth0 (ONBOARD_LAN_IFACE is empty)"
|
||
else
|
||
ok "van: unmanaged-devices has no eth0"
|
||
fi
|
||
```
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: several `FAIL` lines from check 5 (files not yet templated); check 2 may already pass; `exit=1`.
|
||
|
||
- [ ] **Step 2: Turn the hardcoded values into tokens**
|
||
|
||
Make these exact edits:
|
||
|
||
| File | Old | New |
|
||
|---|---|---|
|
||
| `ap/nftables.conf` | `ip saddr 10.42.1.0/24 oifname != "br0" masquerade;` | `ip saddr @GATEWAY_NETWORK@ oifname != "br0" masquerade;` |
|
||
| `ap/van-br0-setup.service` | `brctl addif br0 enP2p33s0 2>/dev/null \|\| true` | `brctl addif br0 @LAN_IFACE@ 2>/dev/null \|\| true` |
|
||
| `ap/van-br0-setup.service` | `brctl addif br0 wlp1s0 2>/dev/null \|\| true` | `brctl addif br0 @WIFI_5G_IFACE@ 2>/dev/null \|\| true` |
|
||
| `failover/config.json` | `"device": "wlan0",` | `"device": "@WIFI_WAN_IFACE@",` |
|
||
| `router-status/config.json` | `"wifi_wan_iface": "wlan0"` | `"wifi_wan_iface": "@WIFI_WAN_IFACE@"` |
|
||
| `ap/50-van-wan.yaml` | ` wlan0:` (the line under `wifis:`) | ` @WIFI_WAN_IFACE@:` |
|
||
| `failover/van-wlan-watchdog.service` | `ExecStart=/usr/local/sbin/van-wlan-watchdog` | `ExecStart=/usr/local/sbin/van-wlan-watchdog @WIFI_WAN_IFACE@` |
|
||
| `ap/van-ap-unmanaged.conf` | `interface-name:@LAN_IFACE@;interface-name:eth0` | `interface-name:@LAN_IFACE@@ONBOARD_LAN_UNMANAGED@` |
|
||
| `ap/23-van-lan-eth0.network` | `Name=eth0` | `Name=@ONBOARD_LAN_IFACE@` |
|
||
| `power/nvme-watch-config.json` | `"nvme_device": "nvme0",` | `"nvme_device": "@NVME_DEVICE@",` |
|
||
| `power/nvme-watch-config.json` | `"fs_device": "/dev/nvme0n1p2",` | `"fs_device": "@NVME_FS_DEVICE@",` |
|
||
|
||
Note: `failover/config.json` has `"device": "wlan0", "metric": 100` — keep the column spacing; only replace the `wlan0` text. The `van-wlan-watchdog` script already takes the device as `argv[1]` (`DEVICE = sys.argv[1] if len(sys.argv) > 1 else "wlan0"`), so no script change is needed.
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: check 2 now FAILs for the files still installed with plain `install`; check 5 passes.
|
||
|
||
- [ ] **Step 3: Install them through `install_rendered` in `deploy.sh`**
|
||
|
||
Replace these lines (spacing preserved as in the file):
|
||
|
||
```bash
|
||
install -D -m0644 ap/nftables.conf /etc/nftables.conf
|
||
```
|
||
→
|
||
```bash
|
||
install_rendered ap/nftables.conf /etc/nftables.conf
|
||
```
|
||
|
||
```bash
|
||
install -D -m0644 ap/van-br0-setup.service /etc/systemd/system/van-br0-setup.service
|
||
```
|
||
→
|
||
```bash
|
||
install_rendered ap/van-br0-setup.service /etc/systemd/system/van-br0-setup.service
|
||
```
|
||
|
||
```bash
|
||
install -D -m0644 failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service
|
||
```
|
||
→
|
||
```bash
|
||
install_rendered failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service
|
||
```
|
||
|
||
```bash
|
||
install -D -m0644 power/nvme-watch-config.json /etc/van-nvme-watch/config.json
|
||
```
|
||
→
|
||
```bash
|
||
install_rendered power/nvme-watch-config.json /etc/van-nvme-watch/config.json
|
||
```
|
||
|
||
and the `23-van-lan-eth0.network` line:
|
||
|
||
```bash
|
||
install -D -m0644 ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network
|
||
```
|
||
→
|
||
```bash
|
||
if [ -n "${ONBOARD_LAN_IFACE:-}" ]; then
|
||
install_rendered ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network
|
||
else
|
||
rm -f /etc/systemd/network/23-van-lan-eth0.network
|
||
fi
|
||
```
|
||
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: every line `ok`, `exit=0`.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add -A ap failover router-status power deploy.sh tests
|
||
git commit -m "Template remaining host-specific values (WAN wifi, LAN/eth0, NVMe, nftables)
|
||
|
||
nftables.conf hardcoded the R5C network, van-br0-setup.service the R5C
|
||
interfaces and several configs wlan0/eth0. All now come from the host
|
||
config; the onboard LAN port is skipped when ONBOARD_LAN_IFACE is empty.
|
||
|
||
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Feature flags, safer apply step, `--diff`
|
||
|
||
**Files:**
|
||
- Modify: `deploy.sh` (package list, cockpit/li3/nvme/HA/frigate blocks, enable/start/restart lines, apply step, new `--diff`, dirty-config warning)
|
||
- Modify: `tests/check-templates.sh` (add checks 6 and 8)
|
||
|
||
**Interfaces:**
|
||
- Consumes (Task 1): `load_conf`, `render`, `warn` (defined later in `deploy.sh`, so the dirty-config warning goes right after its definition), flags `ENABLE_COCKPIT|LI3|NVME_WATCH|HA|FRIGATE`.
|
||
- Produces: `./deploy.sh --diff [conf]` (prints `NEW <dst>` / `CHANGED <dst>` + unified diff for every `install_rendered <src> /abs/dst` line); all flagged units are guarded by a column-0 `if [ "$ENABLE_X" = 1 ]; then … fi` block with 4-space-indented contents (the awk check in check 6 depends on that formatting).
|
||
|
||
- [ ] **Step 1: Add checks 6 and 8 and see them fail**
|
||
|
||
Insert before the final `exit "$fail"`:
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: check 6 lists many unguarded mentions, check 8 FAILs (`--diff` unknown), `exit=1`.
|
||
|
||
- [ ] **Step 2: Add `--diff` and the dirty-config warning**
|
||
|
||
In the `case "${1:-}" in` block (added in Task 1), add before `render)`:
|
||
|
||
```bash
|
||
--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 ;;
|
||
```
|
||
|
||
Directly after the `write_warnings() { … }` function definition in `deploy.sh`, add:
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
- [ ] **Step 3: Gate the optional sections**
|
||
|
||
Edit `deploy.sh` (old text is exact; new text indents block contents by 4 spaces):
|
||
|
||
**Packages** — remove `podman` from the array and add the conditional. Replace
|
||
```bash
|
||
zerotier-one
|
||
podman
|
||
)
|
||
```
|
||
with
|
||
```bash
|
||
zerotier-one
|
||
)
|
||
if [ "$ENABLE_HA" = 1 ]; then
|
||
REQUIRED_PACKAGES+=(podman)
|
||
fi
|
||
```
|
||
|
||
**Cockpit** — replace the first line `if [ "$SKIP_COCKPIT" -ne "1" ]; then` with `if [ "$ENABLE_COCKPIT" = 1 ]; then` (the block is already indented; also fix its stray trailing spaces after the closing `fi`).
|
||
|
||
**Li3** — replace the whole `echo "== li3 …"` section (from `echo "== li3 (RV house battery BMS -> MQTT/HA) =="` through the `python3 -c 'import bleak' …|| warn "python3-bleak missing …"` line) with:
|
||
|
||
```bash
|
||
if [ "$ENABLE_LI3" = 1 ]; then
|
||
echo "== li3 (RV house battery BMS -> MQTT/HA) =="
|
||
# 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
|
||
# battery, over its BLE HM-10 UART module (no pairing) -> Home Assistant MQTT
|
||
# discovery. Broker + creds live only on the system (0600, seeded from
|
||
# li3/config.json.example — same pattern as gps/pushover).
|
||
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-restart-limit.conf /etc/systemd/system/van-li3-battery.service.d/restart-limit.conf
|
||
if [ ! -f /etc/van-li3/config.json ]; then
|
||
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)"
|
||
fi
|
||
python3 -c 'import bleak' 2>/dev/null \
|
||
|| warn "python3-bleak missing (apt install python3-bleak) — van-li3-battery won't start"
|
||
fi
|
||
```
|
||
|
||
and delete the now-duplicated line
|
||
`install -D -m0644 li3/van-li3-battery-restart-limit.conf /etc/systemd/system/van-li3-battery.service.d/restart-limit.conf`
|
||
from the router-status section.
|
||
|
||
**NVMe** — replace the three-line nvme section (after `echo "== nvme watchdog =="`) with:
|
||
|
||
```bash
|
||
if [ "$ENABLE_NVME_WATCH" = 1 ]; then
|
||
echo "== nvme watchdog =="
|
||
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 -D -m0644 power/van-nvme-watch.service /etc/systemd/system/van-nvme-watch.service
|
||
fi
|
||
```
|
||
|
||
**Home Assistant / ESPHome / Frigate** — replace the section from `echo "== home assistant =="` through the `install -d -m0755 /srv/frigate/config /srv/frigate/storage` line with:
|
||
|
||
```bash
|
||
echo "== home assistant =="
|
||
# Native HA (Podman Quadlet, replaced the ha_van VM). daemon-reload below
|
||
# regenerates homeassistant.service; started (not restarted) at the end so a
|
||
# 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 -m0755 /srv/homeassistant
|
||
# ESPHome dashboard (sibling container, same rationale as HA above — no
|
||
# Supervisor/add-on store here).
|
||
install -D -m0644 ha/esphome.container /etc/containers/systemd/esphome.container
|
||
install -d -m0755 /srv/esphome
|
||
fi
|
||
# 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 -m0755 /srv/frigate/config /srv/frigate/storage
|
||
fi
|
||
```
|
||
|
||
**Apply step** — replace the long `systemctl enable regdomain.service … van-router-mqtt >/dev/null 2>&1 || true` line with:
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
Replace the three `systemctl start homeassistant/esphome/frigate … || warn …` lines with:
|
||
|
||
```bash
|
||
if [ "$ENABLE_HA" = 1 ]; then
|
||
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)"
|
||
fi
|
||
if [ "$ENABLE_FRIGATE" = 1 ]; then
|
||
systemctl start frigate || warn "frigate failed to start (podman/quadlet — check journalctl -u frigate)"
|
||
fi
|
||
```
|
||
|
||
Replace the restart lines `systemctl restart van-thermal` … `systemctl restart van-router-mqtt` (thermal, nvme-watch, sms-watch, gps-owntracks, li3-battery, router-mqtt) with:
|
||
|
||
```bash
|
||
systemctl restart van-thermal
|
||
if [ "$ENABLE_NVME_WATCH" = 1 ]; then
|
||
systemctl restart van-nvme-watch
|
||
fi
|
||
systemctl restart van-sms-watch
|
||
systemctl restart van-gps-owntracks
|
||
if [ "$ENABLE_LI3" = 1 ]; then
|
||
systemctl restart van-li3-battery
|
||
fi
|
||
systemctl restart van-router-mqtt
|
||
```
|
||
|
||
Replace `systemctl restart van-ap-dnsmasq nftables van-failover van-wlan-watchdog` with (a failing unit is now recorded as a warning instead of aborting the script mid-apply):
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: all `ok`, `exit=0`. If check 6 still names a line, that line mentions a flagged unit outside a column-0 guard: move it inside a guard (or, if it is a comment/echo, it is already skipped).
|
||
|
||
Also: `bash -n deploy.sh && ./deploy.sh --diff hosts/wan.conf | head -5` — expected: `NEW /etc/...` lines (this laptop has none of the files).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add deploy.sh tests
|
||
git commit -m "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>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Backup scripts read the host config
|
||
|
||
**Files:**
|
||
- Modify: `failover/sync-usb-backup.sh`, `failover/sync-sd-backup.sh`
|
||
- Modify: `tests/check-templates.sh` (add check 7)
|
||
|
||
**Interfaces:**
|
||
- Consumes (Task 1): host variables `USB_BACKUP_ROOT_PART`, `USB_BACKUP_MOUNT`, `SD_BACKUP_UUID`; env override `VANLINK_CONF` (default `/etc/vanlink.conf`).
|
||
- Produces: both scripts exit non-zero with a message naming the missing variable, **before** the root check, when their setting is absent.
|
||
|
||
- [ ] **Step 1: Add check 7 and see it fail**
|
||
|
||
Insert before the final `exit "$fail"`:
|
||
|
||
```bash
|
||
# 7. backup scripts take their settings from the host config and say what is missing
|
||
empty=$(mktemp); trap 'rm -f "$empty"' EXIT
|
||
for pair in "failover/sync-usb-backup.sh:USB_BACKUP_ROOT_PART" "failover/sync-sd-backup.sh:SD_BACKUP_UUID"; do
|
||
s=${pair%%:*}; v=${pair##*:}
|
||
if out=$(VANLINK_CONF="$empty" "./$s" 2>&1); then
|
||
bad "$s should fail when $v is not set"
|
||
elif echo "$out" | grep -q "$v"; then
|
||
ok "$s names missing $v"
|
||
else
|
||
bad "$s failed but did not mention $v"; echo "$out" | sed 's/^/ /'
|
||
fi
|
||
done
|
||
if grep -nE '/dev/sda2|10\.42\.[01]\.44|39118305-4927' failover/sync-*-backup.sh; then
|
||
bad "hardcoded host values remain in the backup scripts"
|
||
else
|
||
ok "no hardcoded devices/UUIDs/addresses in the backup scripts"
|
||
fi
|
||
```
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: check 7 FAILs (scripts still hardcode values and demand root first), `exit=1`.
|
||
|
||
- [ ] **Step 2: Edit `failover/sync-usb-backup.sh`**
|
||
|
||
Directly after `set -euo pipefail`, insert:
|
||
|
||
```bash
|
||
# Backup settings come from the host config (see hosts/vanlink.conf.example).
|
||
# shellcheck disable=SC1090
|
||
. "${VANLINK_CONF:-/etc/vanlink.conf}"
|
||
: "${USB_BACKUP_ROOT_PART:?set USB_BACKUP_ROOT_PART in /etc/vanlink.conf}"
|
||
: "${USB_BACKUP_MOUNT:?set USB_BACKUP_MOUNT in /etc/vanlink.conf}"
|
||
```
|
||
|
||
Replace this block:
|
||
|
||
```bash
|
||
USB_ROOT_PART=/dev/sda2
|
||
MOUNT_POINT=/mnt/usb-backup-root
|
||
MOUNT_POINT=10.42.1.44:/media/andreas/writable-nvme
|
||
|
||
|
||
[[ "$MOUNT_POINT" == *":"* ]]
|
||
LOCAL_MOUNT=$?
|
||
```
|
||
|
||
with:
|
||
|
||
```bash
|
||
USB_ROOT_PART=$USB_BACKUP_ROOT_PART
|
||
MOUNT_POINT=$USB_BACKUP_MOUNT
|
||
|
||
# host:/path means an already-exported NFS target: nothing to mount locally.
|
||
if [[ "$MOUNT_POINT" == *":"* ]]; then LOCAL_MOUNT=0; else LOCAL_MOUNT=1; fi
|
||
```
|
||
|
||
(This also fixes a latent bug: the old `[[ … ]]` line returned 1 for a local mount point and `set -e` aborted the script before `LOCAL_MOUNT=$?` ran.)
|
||
|
||
- [ ] **Step 3: Edit `failover/sync-sd-backup.sh`**
|
||
|
||
Directly after `set -euo pipefail`, insert:
|
||
|
||
```bash
|
||
# Backup settings come from the host config (see hosts/vanlink.conf.example).
|
||
# shellcheck disable=SC1090
|
||
. "${VANLINK_CONF:-/etc/vanlink.conf}"
|
||
: "${SD_BACKUP_UUID:?set SD_BACKUP_UUID in /etc/vanlink.conf}"
|
||
```
|
||
|
||
Replace `SD_UUID=39118305-4927-47a4-acc7-ba022f37af22` with `SD_UUID=$SD_BACKUP_UUID`.
|
||
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"`
|
||
Expected: all `ok`, `exit=0`.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add failover tests
|
||
git commit -m "Backup scripts read device/UUID/target from the host config
|
||
|
||
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Documentation and stale references
|
||
|
||
**Files:**
|
||
- Modify: `README.md` (header), `deploy.sh` (header comment), and comments that mention `deploy.conf`: `failover/van-modem-watch`, `failover/60-van-wan-dns`, `ap/50-van-wan.yaml`, `ap/hostapd-2g.service`, `ap/99-van-router-dns.conf`, `cockpit/vanrouter/vanrouter.js`
|
||
|
||
- [ ] **Step 1: Find the stale references**
|
||
|
||
Run: `grep -rnI 'deploy\.conf' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs .`
|
||
Expected: comment-only hits in the files listed above (and none that are code).
|
||
|
||
- [ ] **Step 2: Update them**
|
||
|
||
For each hit, replace `deploy.conf` with `/etc/vanlink.conf` (comment text only), e.g.:
|
||
|
||
```bash
|
||
grep -rlI 'deploy\.conf' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . \
|
||
| xargs sed -i "s#deploy\.conf's#/etc/vanlink.conf's#g; s#deploy\.conf#/etc/vanlink.conf#g"
|
||
```
|
||
|
||
Replace the first lines of `deploy.sh`:
|
||
|
||
```bash
|
||
# Deploy vanlink configs/scripts (Pi 4 "wan" port) to their system locations.
|
||
# Usage: cd ~/vanlink && sudo ./deploy.sh
|
||
# sudo ./deploy.sh render <file> # preview a templated file on stdout
|
||
```
|
||
with:
|
||
```bash
|
||
# Deploy vanlink configs/scripts to their system locations. One checkout is
|
||
# shared by every host; host-specific values come from /etc/vanlink.conf
|
||
# (symlink to hosts/<hostname>.conf — see hosts/vanlink.conf.example).
|
||
# Usage: cd ~/vanlink && sudo ./deploy.sh
|
||
# ./deploy.sh --check [conf] # validate conf + render all templates, no install
|
||
# ./deploy.sh --diff [conf] # what would change in /etc
|
||
# ./deploy.sh render <file> # preview a templated file on stdout
|
||
```
|
||
|
||
Replace the note block at the top of `README.md` (the `> **This clone = the Pi 4 port …` blockquote, 5 lines) with:
|
||
|
||
```markdown
|
||
> **One checkout, several hosts.** The Pi 5 (`wan`, 10.42.0.1) and the FriendlyElec R5C
|
||
> (`van`, 10.42.1.1) run from this same repo and branch. Everything host-specific
|
||
> (interface names, addressing, which optional services run) lives in
|
||
> `hosts/<hostname>.conf`; `/etc/vanlink.conf` is a symlink to it:
|
||
> `sudo ln -sfn "$PWD/hosts/$(hostname -s).conf" /etc/vanlink.conf`.
|
||
> Edit that file (and commit it), not the individual configs. Before a deploy:
|
||
> `./deploy.sh --check` and `./deploy.sh --diff`. Static checks: `tests/check-templates.sh`.
|
||
```
|
||
|
||
- [ ] **Step 3: Test and commit**
|
||
|
||
Run: `./tests/check-templates.sh; echo "exit=$?"` — expected `exit=0`.
|
||
|
||
```bash
|
||
git add -A README.md deploy.sh failover ap cockpit
|
||
git commit -m "Docs: shared checkout model, /etc/vanlink.conf references
|
||
|
||
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"
|
||
git push origin wan
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Roll out to the R5C (`van`) — needs the user's go-ahead
|
||
|
||
**Gate — ask the user first.** Applying changes `enP1p17s0` (today the port carrying the management address `192.168.10.211`) into the LAN bridge port. Confirm: (a) the cabling is final (Starlink/upstream cable on `enP2p33s0`, LAN devices on `enP1p17s0`), and (b) the user has console access or another way in (e.g. the van Wi-Fi AP at `10.42.1.1`) in case SSH over `192.168.10.211` drops.
|
||
|
||
**Files:** none in the repo; changes on the host `192.168.10.211` (`root@`).
|
||
|
||
- [ ] **Step 1: Put the checkout on the host**
|
||
|
||
`git` is not installed on the R5C. Install it and use the repo remote; if that key/network is unavailable, rsync the laptop checkout including `.git`:
|
||
|
||
```bash
|
||
ssh root@192.168.10.211 'apt-get install -y git'
|
||
# preferred:
|
||
ssh root@192.168.10.211 'cd /home/andreas && git clone -b wan git@git.wrede.ca:andreas/vanlink.git vanlink.new'
|
||
# fallback (laptop checkout is up to date after Task 5's push):
|
||
rsync -a --exclude __pycache__ /home/andreas/vanlink/ root@192.168.10.211:/home/andreas/vanlink.new/
|
||
ssh root@192.168.10.211 'chown -R andreas:andreas /home/andreas/vanlink.new'
|
||
```
|
||
|
||
- [ ] **Step 2: Swap the directory (keep the old one) and link the host config**
|
||
|
||
```bash
|
||
ssh root@192.168.10.211 'cd /home/andreas && mv vanlink vanlink.old && mv vanlink.new vanlink &&
|
||
ln -sfn /home/andreas/vanlink/hosts/van.conf /etc/vanlink.conf && ls -l /etc/vanlink.conf'
|
||
```
|
||
Expected: `/etc/vanlink.conf -> /home/andreas/vanlink/hosts/van.conf`.
|
||
|
||
- [ ] **Step 3: Validate and preview**
|
||
|
||
```bash
|
||
ssh root@192.168.10.211 'cd /home/andreas/vanlink && ./deploy.sh --check && ./deploy.sh --diff'
|
||
```
|
||
Expected: `check OK: N templates render cleanly`, then `NEW`/`CHANGED` entries. Read every `CHANGED` diff: the expected ones are `/etc/van-ap/dnsmasq.conf`, `/etc/systemd/network/21-van-br0.network`, `/etc/nftables.conf` (previously unrendered), `05-van-lan.network` (NEW), and `/etc/systemd/system/van-br0-setup.service`. Anything else needs an explanation before continuing.
|
||
|
||
- [ ] **Step 4: Deploy and verify**
|
||
|
||
```bash
|
||
ssh root@192.168.10.211 'cd /home/andreas/vanlink && ./deploy.sh 2>&1 | tail -25'
|
||
```
|
||
Then (from a machine that can still reach it; use `10.42.1.1` if `192.168.10.211` is gone):
|
||
|
||
```bash
|
||
ssh root@<host> 'systemctl is-active van-ap-dnsmasq nftables hostapd; ip -br addr show br0; networkctl status enP1p17s0 | grep "Network File"; nft list ruleset | grep masquerade'
|
||
```
|
||
Expected: `active` ×3; `br0 … 10.42.1.1/24`; `Network File: /etc/systemd/network/05-van-lan.network`; a masquerade rule for `10.42.1.0/24`. Report anything else to the user (do not paper over failures; `journalctl -xeu <unit>`).
|
||
|
||
- [ ] **Step 5: Record the outcome in memory** (`multihost-config-plan`): R5C migrated, old copy at `/home/andreas/vanlink.old`.
|
||
|
||
---
|
||
|
||
### Task 7: Roll out to the Pi 5 (`wan`) — needs the user's go-ahead
|
||
|
||
**Gate — ask the user first.** A deploy restarts `systemd-networkd`/hostapd on a live router (brief Wi-Fi/LAN outage) — agree a moment. Also decide with the user: `ENABLE_NVME_WATCH=0` in `hosts/wan.conf` means deploy no longer manages `van-nvme-watch`; the Pi has no NVMe yet the service is currently running. Ask whether to `systemctl disable --now van-nvme-watch` there (deploy never stops services).
|
||
|
||
**Files:** none in the repo; changes on `root@10.42.0.1`. Run git there as `git -c safe.directory=/home/andreas/vanlink` (repo owned by `andreas`, commands run as root).
|
||
|
||
- [ ] **Step 1: Back up the local edits, then drop them**
|
||
|
||
```bash
|
||
ssh root@10.42.0.1 'cd /home/andreas/vanlink && G="git -c safe.directory=/home/andreas/vanlink" &&
|
||
mkdir -p /home/andreas/vanlink-pre-migration && cp -a deploy.conf deploy.sh failover/sync-usb-backup.sh li3/van-li3-battery /home/andreas/vanlink-pre-migration/ &&
|
||
$G diff > /home/andreas/vanlink-pre-migration/local-edits.diff && $G status --short'
|
||
```
|
||
Expected: the four modified files (`deploy.conf`, `deploy.sh`, `failover/sync-usb-backup.sh`, `li3/van-li3-battery`) and nothing else. Their content is superseded: `li3` and `sync-usb-backup.sh` changes are already in `wan` (`565e8f3`); the host values are now in `hosts/wan.conf`; the `SKIP_LI3` logic became `ENABLE_LI3`. Confirm with the user, then:
|
||
|
||
```bash
|
||
ssh root@10.42.0.1 'cd /home/andreas/vanlink && G="git -c safe.directory=/home/andreas/vanlink" &&
|
||
$G checkout -- . && $G fetch origin && $G merge --ff-only origin/wan && $G log --oneline -1'
|
||
```
|
||
Expected: fast-forward to the commit pushed in Task 5.
|
||
|
||
- [ ] **Step 2: Link the host config, validate, and compare with what is live**
|
||
|
||
```bash
|
||
ssh root@10.42.0.1 'cd /home/andreas/vanlink && ln -sfn "$PWD/hosts/wan.conf" /etc/vanlink.conf &&
|
||
./deploy.sh --check && ./deploy.sh --diff'
|
||
```
|
||
Expected: `check OK`. Read every `CHANGED` entry; changes that alter behaviour on the Pi 5 (addresses, interface names, DNS, unit contents) must be explained or `hosts/wan.conf` corrected **before** deploying. Fix by editing `hosts/wan.conf` on the laptop, commit, push, `git merge --ff-only` again.
|
||
|
||
- [ ] **Step 3: Deploy in the agreed window and verify**
|
||
|
||
```bash
|
||
ssh root@10.42.0.1 'cd /home/andreas/vanlink && ./deploy.sh 2>&1 | tail -25'
|
||
ssh root@10.42.0.1 'systemctl is-active van-ap-dnsmasq nftables hostapd hostapd-2g van-failover; ip -br addr show br0; cat /run/van-failover/state.json | head -20'
|
||
```
|
||
Expected: all `active`, `br0 … 10.42.0.1/24`, failover state healthy. Any `warn` lines from deploy go to the user.
|
||
|
||
- [ ] **Step 4: Update memory** (`multihost-config-plan`): both hosts migrated; `/etc/vanlink.conf` symlinks; backup of the old Pi edits at `/home/andreas/vanlink-pre-migration`. Delete `vanlink.old` on the R5C only when the user says so.
|
||
|
||
---
|
||
|
||
## Self-Review (done)
|
||
|
||
**Spec coverage:** host file + symlink (Task 1, 6, 7); variable table incl. new vars (Task 1 arrays/example, Task 2 tokens); repo changes / `deploy.conf` removal / hosts files / new tokens (Tasks 1-2); README (Task 5); `deploy.sh` behaviour: missing/dangling symlink message + required-var abort + resolved path + dirty warning (Tasks 1, 3), `install_rendered` leftover guard (Task 1), `05-van-lan` and eth0 handling (pending-fix commit, Task 2), feature flags for install/enable/restart (Task 3), `--check` (Task 1), `render` (Task 1), restart-failure warnings (Task 3); backup scripts (Task 4); testing items 1-4 (checks 1-4) plus content checks; migration (Tasks 6-7) incl. pre-deploy diff (`--diff`, Task 3) and Pi 5 backup; out-of-scope items untouched.
|
||
|
||
**Placeholders:** none; every code step has its code. Task 6/7 are operational and hold explicit gates.
|
||
|
||
**Type/name consistency:** `LAN_IFACE`, `WIFI_WAN_IFACE`, `ONBOARD_LAN_IFACE`/`ONBOARD_LAN_UNMANAGED`, `NVME_DEVICE`/`NVME_FS_DEVICE`, `USB_BACKUP_ROOT_PART`/`USB_BACKUP_MOUNT`, `SD_BACKUP_UUID`, `ENABLE_*` names identical across the spec, `REQUIRED_VARS`/`OPTIONAL_VARS`, host files, templates and tests. Check numbers in the test script are 1, 2, 3, 4, 5, 6, 7, 8 (a section is added in the task that introduces its behaviour; numbering follows the spec's order where it exists and is not required to be contiguous while tasks are in progress).
|