diff --git a/ap/22-van-lan.network b/ap/22-van-lan.network index dc34fcd..8a9c251 100644 --- a/ap/22-van-lan.network +++ b/ap/22-van-lan.network @@ -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 diff --git a/ap/van-ap-unmanaged.conf b/ap/van-ap-unmanaged.conf index 16a2eae..4297a8e 100644 --- a/ap/van-ap-unmanaged.conf +++ b/ap/van-ap-unmanaged.conf @@ -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 diff --git a/cockpit/vanrouter/vanrouter.js b/cockpit/vanrouter/vanrouter.js index 3bf4f82..ffa75eb 100644 --- a/cockpit/vanrouter/vanrouter.js +++ b/cockpit/vanrouter/vanrouter.js @@ -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" }; diff --git a/deploy.conf b/deploy.conf deleted file mode 100644 index b1c7428..0000000 --- a/deploy.conf +++ /dev/null @@ -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 ` - -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" diff --git a/deploy.sh b/deploy.sh index 88d37b1..0749caa 100755 --- a/deploy.sh +++ b/deploy.sh @@ -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 # 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/.conf in this repo. Config +# files below carry @TOKEN@ placeholders substituted from those variables via +# render()/install_rendered() — edit hosts/.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 — 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 -> 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 — 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 [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 - [ -n "${2:-}" ] || { echo "Usage: $0 render "; exit 1; } - render "$2" - exit 0 -fi +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 "; exit 1; } + load_conf "$VANLINK_CONF" + render "$2" + 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 ==" diff --git a/hosts/van.conf b/hosts/van.conf new file mode 100644 index 0000000..304d4b8 --- /dev/null +++ b/hosts/van.conf @@ -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 diff --git a/hosts/vanlink.conf.example b/hosts/vanlink.conf.example new file mode 100644 index 0000000..8b398f1 --- /dev/null +++ b/hosts/vanlink.conf.example @@ -0,0 +1,41 @@ +# hosts/vanlink.conf.example — reference for /etc/vanlink.conf. +# +# Each host has hosts/.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/.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) diff --git a/hosts/wan.conf b/hosts/wan.conf new file mode 100644 index 0000000..d7fd2f9 --- /dev/null +++ b/hosts/wan.conf @@ -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 diff --git a/router-status/config.json b/router-status/config.json index fa1fa07..4b26431 100644 --- a/router-status/config.json +++ b/router-status/config.json @@ -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@", diff --git a/tests/check-templates.sh b/tests/check-templates.sh new file mode 100755 index 0000000..018010e --- /dev/null +++ b/tests/check-templates.sh @@ -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"