#!/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
    ;;
  dc1)
    # MEASURED / apex-committed at the dc1 standup (2026-07-22..23):
    #   vr1-dc1-metal-admin      10.12.68.2/22  rack MAAS/DHCP leg -- the
    #     apex-committed rack IP (docs/audit/dc1-rack-import-commit-20260722.txt;
    #     NetBox dcim.site vr1-dc1), D-124 amendment plane map.
    #   vr1-dc1-metal-admin      10.12.68.3/22  node-DNS forwarder listen alias
    #     -- D-120 static, below the .100-.200 dynamic range (dc0 .3 analog).
    #   vr1-dc1-provider-public  10.12.64.2/22  edge-LAN leg -- MEASURED live on
    #     the provider-public bridge 2026-07-23 (edge gw 10.12.64.1; lib-net.sh
    #     vr1-dc1 arm, PLANE_GW ["10.12.64.0/22"]="10.12.64.1").
    LEGS="vr1-dc1-metal-admin=10.12.68.2/22
vr1-dc1-metal-admin=10.12.68.3/22
vr1-dc1-provider-public=10.12.64.2/22"
    DNS_LISTEN="10.12.68.3"         # MEASURED: forwarder listen alias (metal-admin)
    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
