#!/usr/bin/env bash
# scripts/dc-rack-net.sh <check|install> <site> -- DC rack network persistence:
# rack bridge legs + the node-facing DNS forwarder (D-131 sub-1, RULED
# 2026-07-21: the forwarder is the STANDING per-DC pattern; this script is its
# repo-carried delivery, folding in rack-legs persistence per D-128 register
# item 20).
#
# RUNS ON THE DC RACK HOST (e.g. vvr1-dc0), not on vcloud/voffice1. Invoke
# from a repo host over ssh, piping the script (no repo clone needed there):
#
# check: ssh -i <dc0-key> -J voffice1 <user>@172.31.0.2 \
# 'sudo bash -s' -- check dc0 < scripts/dc-rack-net.sh
# install: same with 'install dc0' (idempotent; safe to re-run).
#
# WHAT IT OWNS (site-keyed table below; every value MEASURED, never inferred):
# 1. <site>-rack-legs.service -- oneshot, After=libvirtd: re-adds the rack's
# bridge IP legs on every boot. WHY: these addresses were added with bare
# `ip addr` at standup (2026-07-20/21 session changelogs) and are DROPPED
# on reboot -- same failure class as the D-126 office1 base leg. The rack's
# MAAS DHCP (metal-admin leg) and edge-LAN reach (provider-public leg)
# silently die with them.
# 2. <site>-node-dns.service + /etc/dnsmasq-<site>-node.conf -- the D-131
# forwarder: dnsmasq on a metal-admin alias, no-resolv, forwarding to the
# region's authoritative BIND over the rack's own transit connection
# (OUTPUT traffic only -- the SEC-010 FORWARD-drop is untouched). WHY: the
# MAAS 3.7 rack-only agent resolver SERVFAILs ALL queries on an
# internet-isolated rack, including its own authoritative maas-internal
# zone (walks public root hints, ignores resolv.conf; LP report queued),
# so commissioning cloud-config-url never resolves. Full chain:
# docs/audit/commissioning-diag-20260721.txt.
# 3. /usr/local/sbin/<site>-rack-net-apply -- the boot-time helper both units
# use. It resolves bridge devices FROM LIBVIRT NETWORK NAMES at runtime
# (virsh net-dumpxml): the virbrN names are libvirt AUTO-ASSIGNED and
# would drift if a plane network were ever recreated -- a network NAME is
# the stable identity, a virbrN literal is exactly the drifting-ID class
# hard rule 3 forbids. No virbr literal appears in this file.
#
# REGION-SIDE COMPANION (runbook step, NOT this script -- same split as
# site-headend-install.sh's DHCP note): the MAAS metal-admin subnet must carry
# dns_servers=<forwarder addr> + allow_dns=false (applied + proven 2026-07-21).
#
# EXIT: 0 ok | 1 check failed | 2 bad args/unknown site | 4 install failed.
# ASCII + LF only.
set -uo pipefail
MODE="${1:-}"; SITE="${2:-}"
case "$MODE" in check|install) ;; *)
echo "usage: dc-rack-net.sh <check|install> <site>" >&2; exit 2 ;; esac
# ---------------------------------------------------------------------------
# Site table. ADD A SITE ONLY WITH MEASURED VALUES (hard rule 2): every leg row
# cites where its value was measured. The harness rejects rows without a
# MEASURED tag.
# ---------------------------------------------------------------------------
case "$SITE" in
dc0)
# MEASURED 2026-07-21 on vvr1-dc0 (ip -4 -o addr / virsh net-list; session
# changelog 2026-07-21 + adjudication commissioning-diag-20260721.txt):
# vr1-dc0-metal-admin 10.12.8.2/22 rack MAAS/DHCP leg
# vr1-dc0-metal-admin 10.12.8.3/22 node-DNS forwarder listen alias
# vr1-dc0-provider-public 10.12.4.2/22 edge-LAN leg (edge gw 10.12.4.1)
LEGS="vr1-dc0-metal-admin=10.12.8.2/22
vr1-dc0-metal-admin=10.12.8.3/22
vr1-dc0-provider-public=10.12.4.2/22"
DNS_LISTEN="10.12.8.3" # MEASURED: live forwarder listen address
DNS_UPSTREAM="10.10.0.20" # MEASURED: region BIND (voffice1) over transit
;;
*) echo "FAIL: unknown site '$SITE' -- add a MEASURED row block first" >&2; exit 2 ;;
esac
HELPER="/usr/local/sbin/${SITE}-rack-net-apply"
LEGS_UNIT="/etc/systemd/system/${SITE}-rack-legs.service"
DNS_CONF="/etc/dnsmasq-${SITE}-node.conf"
DNS_UNIT="/etc/systemd/system/${SITE}-node-dns.service"
# ---------------------------------------------------------------------------
# Generated file contents (single source of truth for check AND install).
# ---------------------------------------------------------------------------
gen_helper() {
cat <<EOF
#!/usr/bin/env bash
# ${SITE}-rack-net-apply -- generated by scripts/dc-rack-net.sh; do not hand-edit.
# Re-adds the rack bridge legs. Bridge devices are resolved from libvirt
# NETWORK NAMES at runtime (virbrN is auto-assigned and not stable identity).
set -euo pipefail
br_of() {
virsh -c qemu:///system net-dumpxml "\$1" | sed -n "s/.*bridge name='\\([^']*\\)'.*/\\1/p"
}
apply_legs() {
$(echo "$LEGS" | while IFS='=' read -r net cidr; do
printf ' ip addr replace %s dev "$(br_of %s)"\n' "$cidr" "$net"; done)
}
apply_legs
EOF
}
gen_legs_unit() {
cat <<EOF
[Unit]
Description=${SITE} rack bridge IP legs (dc-rack-net.sh; D-128 item 20 -- re-add on boot)
After=libvirtd.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${HELPER}
[Install]
WantedBy=multi-user.target
EOF
}
gen_dns_conf() {
cat <<EOF
# ${SITE} node-facing DNS forwarder (D-131 sub-1 STANDING pattern; generated by
# scripts/dc-rack-net.sh -- do not hand-edit).
# Why: MAAS 3.7 rack-only agent resolver SERVFAILs authoritative maas-internal
# queries on an internet-isolated rack (walks root hints; LP report queued).
# Nodes need maas-internal names for cloud-init's cloud-config-url.
# Listens on a metal-admin alias and forwards to the region's authoritative
# BIND over the rack's own transit connection (OUTPUT traffic -- the SEC-010
# FORWARD-drop is untouched).
listen-address=${DNS_LISTEN}
bind-interfaces
port=53
no-resolv
no-hosts
server=${DNS_UPSTREAM}
cache-size=500
EOF
}
gen_dns_unit() {
cat <<EOF
[Unit]
Description=${SITE} node-facing DNS forwarder (D-131; MAAS 3.7 rack-resolver workaround)
After=${SITE}-rack-legs.service
Requires=${SITE}-rack-legs.service
[Service]
Type=simple
ExecStart=/usr/sbin/dnsmasq -k -C ${DNS_CONF}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
}
# ---------------------------------------------------------------------------
# check -- mutate NOTHING; report and exit 0 ok / 1 incomplete.
# ---------------------------------------------------------------------------
FAILED=0
say(){ echo " $1"; }
miss(){ echo " MISS $1"; FAILED=1; }
check_file() { # path gen_fn
if [ ! -f "$1" ]; then miss "$1 absent"; return; fi
if diff -q <("$2") "$1" >/dev/null 2>&1; then say "OK $1 matches"; else
miss "$1 DIFFERS from generated content"; fi
}
do_check() {
check_file "$HELPER" gen_helper
check_file "$LEGS_UNIT" gen_legs_unit
check_file "$DNS_CONF" gen_dns_conf
check_file "$DNS_UNIT" gen_dns_unit
for u in "${SITE}-rack-legs" "${SITE}-node-dns"; do
[ "$(systemctl is-enabled "$u" 2>/dev/null)" = "enabled" ] \
&& say "OK $u enabled" || miss "$u not enabled"
done
systemctl is-active --quiet "${SITE}-node-dns" \
&& say "OK ${SITE}-node-dns active" || miss "${SITE}-node-dns not active"
echo "$LEGS" | while IFS='=' read -r net cidr; do
br="$(virsh -c qemu:///system net-dumpxml "$net" 2>/dev/null \
| sed -n "s/.*bridge name='\([^']*\)'.*/\1/p")"
if [ -z "$br" ]; then echo " MISS libvirt net $net unresolvable"; exit 1; fi
ip -4 -o addr show dev "$br" 2>/dev/null | grep -q " ${cidr%%/*}/" \
&& echo " OK $cidr on $br ($net)" \
|| { echo " MISS $cidr absent on $br ($net)"; exit 1; }
done || FAILED=1
[ "$FAILED" -eq 0 ] && { echo "dc-rack-net check ($SITE): PASS"; exit 0; }
echo "dc-rack-net check ($SITE): FAIL"; exit 1
}
# ---------------------------------------------------------------------------
# install -- idempotent; requires root.
# ---------------------------------------------------------------------------
do_install() {
[ "$(id -u)" = "0" ] || { echo "FAIL: install requires root" >&2; exit 4; }
command -v dnsmasq >/dev/null 2>&1 || { echo "FAIL: dnsmasq not installed" >&2; exit 4; }
umask 022
gen_helper > "$HELPER" && chmod 755 "$HELPER" || exit 4
gen_legs_unit > "$LEGS_UNIT" || exit 4
gen_dns_conf > "$DNS_CONF" || exit 4
gen_dns_unit > "$DNS_UNIT" || exit 4
systemctl daemon-reload || exit 4
systemctl enable --now "${SITE}-rack-legs.service" >/dev/null 2>&1 || exit 4
systemctl enable "${SITE}-node-dns.service" >/dev/null 2>&1 || exit 4
systemctl restart "${SITE}-node-dns.service" || exit 4
echo "dc-rack-net install ($SITE): done -- running check:"
do_check
}
case "$MODE" in
check) do_check ;;
install) do_install ;;
esac