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 @TOKENs and gains
--check/--templates. LAN_USB_IFACE is renamed LAN_IFACE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 14:28:11 -04:00
co-authored by Claude Sonnet 5
parent 442010c4b8
commit 0cbbeb7aba
10 changed files with 235 additions and 75 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# gets DHCP/DNS from the same dnsmasq as Wi-Fi clients. NM must leave this device
# alone (see van-ap-unmanaged.conf) for networkd to own it here.
[Match]
Name=@LAN_USB_IFACE@
Name=@LAN_IFACE@
[Link]
RequiredForOnline=no
+1 -1
View File
@@ -2,4 +2,4 @@
# The AP wlans (5GHz + 2.4GHz) and the wired LAN ports (USB dongle + onboard
# eth0) are all owned by systemd-networkd/hostapd (bridged into br0), so
# NetworkManager must not touch them.
unmanaged-devices=interface-name:@WIFI_5G_IFACE@;interface-name:@WIFI_2G_IFACE@;interface-name:@LAN_USB_IFACE@;interface-name:eth0
unmanaged-devices=interface-name:@WIFI_5G_IFACE@;interface-name:@WIFI_2G_IFACE@;interface-name:@LAN_IFACE@;interface-name:eth0
+1 -1
View File
@@ -15,7 +15,7 @@ const APS = [
];
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
// Wired LAN bridge ports: clients behind them are found via the bridge FDB.
const LAN_PORTS = { "eth0": "LAN (eth0)", "@LAN_USB_IFACE@": "LAN (USB)" };
const LAN_PORTS = { "eth0": "LAN (eth0)", "@LAN_IFACE@": "LAN (USB)" };
// Starlink dish: gRPC status API on the fixed management IP (reached via the /32
// link route the netplan profile installs on the RTL8153 uplink).
const STARLINK = { iface: "@STARLINK_IFACE@", dish: "192.168.100.1:9200" };
-47
View File
@@ -1,47 +0,0 @@
# deploy.conf — hardware-instance identifiers for this Pi's vanlink deployment.
#
# These are the values that change when a USB Wi-Fi dongle, LAN adapter, or
# modem gets physically swapped (MAC-derived interface names are stable per
# physical device, but change when the device changes). Edit here and run
# `sudo ./deploy.sh` — it substitutes @TOKEN@ placeholders in the repo's
# config templates with these values before installing them.
#
# Find a new device's interface name after plugging it in: `iw dev` (wifi) or
# `ip -br link` (wired). USB vendor ID: `lsusb`.
# Preview a rendered file without deploying: `sudo ./deploy.sh render <file>`
SKIP_COCKPIT=1
# 5GHz AP radio (currently: Mediatek MT7925E PCIe on FriendlyElec R5C, driver mt76 on Armbian 6.18)
# Note: RTL8822CE (rtw88 driver) does not support bridge mode in AP; MT7925E solved this
WIFI_5G_IFACE=wlp1s0
# 2.4GHz AP radio (currently: same as 5GHz on Armbian R5C; on Debian was D-Link DWA-171)
WIFI_2G_IFACE=wlx3c3332002066
# Wired LAN port, USB-attached gigabit adapter (RTL8153-family)
LAN_USB_IFACE=enP2p33s0
# Starlink dish uplink, USB-attached gigabit adapter (RTL8153-family)
#STARLINK_IFACE=enxd8ec5eeb3512 # White Belkin dongle
#STARLINK_IFACE=enx6c1ff7d210a5 # UGreen 8-port hub's built-in Ethernet (ASIX AX88179B)
STARLINK_IFACE=enP1p17s0
# Cellular modem USB vendor ID (Quectel EC25-AF)
MODEM_USB_VENDOR=2c7c
# Fixed upstream DNS resolvers — this router always uses these, never a WAN's
# own DHCP/RA-provided servers (NetworkManager is told to ignore those
# entirely; see ap/van-wan-dns.conf + ap/99-van-router-dns.conf). Keeps
# resolution identical on Wapana, Starlink, or cellular.
DNS_RESOLVERS="1.1.1.1 8.8.8.8"
# IP address configuration for this router
# When deploying to different hardware, change GATEWAY_IP, DHCP_RANGE_START/END, and GATEWAY_NETWORK.
# Pi 5 (main): 10.42.0.1/24 with DHCP 10.42.0.10-254
# R5C (failover): 10.42.1.1/24 with DHCP 10.42.1.10-254
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"
+92 -22
View File
@@ -2,49 +2,119 @@
# 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
# ./deploy.sh --check [conf] # validate conf + render all templates (no root)
# ./deploy.sh --templates # list templated files
# Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN
# port on br0) is NOT deployed here — reference copy in ap/50-van-wan.yaml,
# applied once manually (apply flaps uplinks). We do check it for drift below.
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"
# deploy.conf holds hardware-instance identifiers (interface names, USB vendor
# IDs) that change when a dongle/adapter gets physically swapped. Config files
# below carry @TOKEN@ placeholders substituted from these variables via
# render()/install_rendered() — edit deploy.conf, not the individual configs.
set -a
source ./deploy.conf
set +a
# 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
sed -e "s|@WIFI_5G_IFACE@|$WIFI_5G_IFACE|g" \
-e "s|@WIFI_2G_IFACE@|$WIFI_2G_IFACE|g" \
-e "s|@LAN_USB_IFACE@|$LAN_USB_IFACE|g" \
-e "s|@STARLINK_IFACE@|$STARLINK_IFACE|g" \
-e "s|@MODEM_USB_VENDOR@|$MODEM_USB_VENDOR|g" \
-e "s|@DNS_RESOLVERS@|$DNS_RESOLVERS|g" \
-e "s|@GATEWAY_IP@|$GATEWAY_IP|g" \
-e "s|@GATEWAY_PREFIX@|$GATEWAY_PREFIX|g" \
-e "s|@GATEWAY_NETWORK@|$GATEWAY_NETWORK|g" \
-e "s|@DHCP_RANGE_START@|$DHCP_RANGE_START|g" \
-e "s|@DHCP_RANGE_END@|$DHCP_RANGE_END|g" \
"$1"
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"
}
if [ "${1:-}" = "render" ]; then
template_files() { # every repo file that carries an @TOKEN@ placeholder
grep -rlE "$TOKEN_RE" --exclude-dir=.git --exclude-dir=__pycache__ --exclude-dir=.superpowers \
--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
fi
exit 0 ;;
esac
[ "$(id -u)" = 0 ] || { echo "Run with sudo (writes to /etc, /usr)."; exit 1; }
load_conf "$VANLINK_CONF"
echo "== host config: $(readlink -f "$VANLINK_CONF") =="
# Check for essential package dependencies
echo "== dependency check =="
+30
View File
@@ -0,0 +1,30 @@
# 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
+41
View File
@@ -0,0 +1,41 @@
# 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)
+30
View File
@@ -0,0 +1,30 @@
# 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
+1 -1
View File
@@ -9,7 +9,7 @@
"poll_interval_s": 5,
"wifi_5g_iface": "@WIFI_5G_IFACE@",
"wifi_2g_iface": "@WIFI_2G_IFACE@",
"lan_usb_iface": "@LAN_USB_IFACE@",
"lan_usb_iface": "@LAN_IFACE@",
"starlink_iface": "@STARLINK_IFACE@",
"starlink_dish": "192.168.100.1:9200",
"modem_usb_vendor": "@MODEM_USB_VENDOR@",
+36
View File
@@ -0,0 +1,36 @@
#!/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"