46 KiB
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 envVANLINK_CONF); host files arehosts/<hostname -s>.conf:hosts/van.conf(R5C, hostnamevan) andhosts/wan.conf(Pi 5, hostnamewan). - 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_DEVICEandNVME_FS_DEVICEare required only whenENABLE_NVME_WATCH=1. ENABLE_*flags are exactly0or1.LAN_USB_IFACEis renamedLAN_IFACEeverywhere; the JSON keylan_usb_ifaceinrouter-status/config.jsonis not renamed (consumer-facing).deploy.shnever creates or edits/etc/vanlink.conforhosts/*.conf.deploy.sh--check,--templates,--diffandrenderneed no root and install nothing.- LAN bridge file installs as
/etc/systemd/network/05-van-lan.network(sorts before netplan's10-file). - Commit messages end with
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>. - Do not touch the
.bakfiles,ap/50-van-wan.yaml's PSK, orpower/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_IFACEexcept.bakanddocs/
Interfaces:
-
Produces (used by all later tasks), in
deploy.sh: arraysREQUIRED_VARS,OPTIONAL_VARS,DERIVED_VARS,TOKEN_VARS; stringTOKEN_RE; functionsdie,load_conf <file>,render <file>,check_rendered <src> <rendered>,install_rendered <src> <dst> [mode],template_files; sub-commands--check [conf],--templates,render <file>. Derived varONBOARD_LAN_UNMANAGED(;interface-name:<iface>or empty). -
Produces in
tests/check-templates.sh: helper functionsok <msg>/bad <msg>and variablefail; later tasks append numbered check sections before the finalexit "$fail". -
Step 1: Commit the pending
deploy.shfix
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.
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):
#!/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:
# 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):
# 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):
# 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.confblock at the top ofdeploy.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:
# 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:
load_conf "$VANLINK_CONF"
echo "== host config: $(readlink -f "$VANLINK_CONF") =="
- Step 5: Rename
LAN_USB_IFACEtoLAN_IFACEin the remaining files and deletedeploy.conf
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:
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
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 varsWIFI_WAN_IFACE,ONBOARD_LAN_IFACE,ONBOARD_LAN_UNMANAGED,NVME_DEVICE,NVME_FS_DEVICE,GATEWAY_NETWORK,LAN_IFACE,WIFI_5G_IFACE. -
Produces:
tests/check-templates.shchecks 2 (every templated file installed viainstall_rendered) and 5 (rendered host-specific content). -
Step 1: Add checks 2 and 5 to the test script
Insert before the final exit "$fail":
# 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_renderedindeploy.sh
Replace these lines (spacing preserved as in the file):
install -D -m0644 ap/nftables.conf /etc/nftables.conf
→
install_rendered ap/nftables.conf /etc/nftables.conf
install -D -m0644 ap/van-br0-setup.service /etc/systemd/system/van-br0-setup.service
→
install_rendered ap/van-br0-setup.service /etc/systemd/system/van-br0-setup.service
install -D -m0644 failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service
→
install_rendered failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service
install -D -m0644 power/nvme-watch-config.json /etc/van-nvme-watch/config.json
→
install_rendered power/nvme-watch-config.json /etc/van-nvme-watch/config.json
and the 23-van-lan-eth0.network line:
install -D -m0644 ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network
→
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
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 indeploy.sh, so the dirty-config warning goes right after its definition), flagsENABLE_COCKPIT|LI3|NVME_WATCH|HA|FRIGATE. -
Produces:
./deploy.sh --diff [conf](printsNEW <dst>/CHANGED <dst>+ unified diff for everyinstall_rendered <src> /abs/dstline); all flagged units are guarded by a column-0if [ "$ENABLE_X" = 1 ]; then … fiblock 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":
# 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
--diffand the dirty-config warning
In the case "${1:-}" in block (added in Task 1), add before render):
--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:
# 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
zerotier-one
podman
)
with
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:
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:
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:
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:
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:
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:
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):
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
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 overrideVANLINK_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":
# 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:
# 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:
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:
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:
# 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
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 mentiondeploy.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.:
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:
# 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:
# 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:
> **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.
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:
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
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
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
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):
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
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:
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
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
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.confsymlinks; backup of the old Pi edits at/home/andreas/vanlink-pre-migration. Deletevanlink.oldon 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).