#!/usr/bin/env bash
# scripts/dc-node-carve.sh <check|apply> <site> --profile <p> --expect-rack <r>
#                          [--host <hostname>] [--commit]
#
# The v4 NODE interface carve for a VR1 DC, against that DC's OWN MAAS region
# (D-132 q1). Re-homes each node's six NICs onto their plane VLANs, builds the
# OVS `br-ex` on the provider uplink of every ROLE node, and lays the D-134
# statics. IPv6 is NOT this script's job -- run `dc-node-v6-carve.py` afterwards
# (it needs br-ex to already exist, because the provider v6 static lands there).
#
# WHY THIS EXISTS (2026-07-30). The equivalent work at Stage 4 was done ad-hoc
# through the MAAS API and logged only to `~/as-executed/2026-07-23-stage4-carve.log`,
# which is NOT in the repo -- so when D-132 q1 moved dc0 to its own region and
# every machine had to be re-enrolled, there was nothing to re-run. This is that
# missing tool. dc1 rebuilds from it rather than from a transcript.
#
# TARGET TREE -- and it is NOT uniform across roles (lib-hosts.sh
# NODE_CARVE_ROLE_PLANES / NODE_CARVE_JUJU_PLANES; measured from the
# pre-migration capture, docs/audit/dc0-maas-carve-premigration-20260730.txt):
#
#   ROLE NODE (the 9 `openstack-<site>` machines), octet N:
#     enp1s0  raw    + STATIC <metal-admin>.N      (PXE leg; auto -> STATIC)
#     enp2s0  ------> br-ex (OVS) + STATIC <provider-public>.N   [Pattern A]
#                     enp2s0 itself holds NO L3 link.
#     enp3s0  raw    + STATIC <metal-internal>.N
#     enp4s0  raw    + STATIC <data-tenant>.N
#     enp5s0  raw    + STATIC <storage>.N
#     enp6s0  raw    + STATIC <replication>.N
#
#   JUJU CONTROLLER (`<site>-juju-01`), octet 5:
#     enp1s0  raw    + STATIC <metal-admin>.5
#     enp2s0  raw    + STATIC <provider-public>.5   -- RAW, no br-ex.
#     enp3s0..enp6s0 DELIBERATELY LEFT on their auto-created VLANs.
#   The controller runs no workloads, so it needs no tenant/storage/replication
#   legs. It DOES need a default route, and the provider-public leg is what
#   supplies it (the provider subnet carries gateway_ip). The absence of that
#   leg is the under-carve that cost three bootstrap attempts on 2026-07-30.
#
# NIC ORDER IS `lib-hosts.sh NIC_PLANE_ORDER`, NEVER `lib-net.sh PLANE_CIDRS`.
# PLANE_CIDRS starts with provider-public; the NIC order starts with metal-admin.
# Walking PLANE_CIDRS positionally swaps enp1s0/enp2s0 and strands PXE on a plane
# with no DHCP. `tests/dc-selector` asserts the two orders differ at position 0
# so that guard cannot go vacuous.
#
# EVERY id is resolved LIVE and keyed by a STABLE identity: machines by PINNED
# BOOT MAC (system_id AND hostname are both re-minted on re-enlistment -- only
# the MAC survives), subnets and VLANs by CIDR, interfaces by NAME. MAAS row ids
# do NOT cross regions: VLAN row 5005 is dc0's metal-admin in the Office1 region
# and `vr1-dc0-data-tenant` in the per-DC one. The pre-migration capture is a
# REBUILD REFERENCE for addresses and interface names ONLY -- never for an id.
#
# A 200-SHAPED MAAS RESPONSE IS NOT EVIDENCE THE STATE CHANGED. `interface update
# ... vlan=` has been measured returning a full JSON object while changing
# nothing, surfacing only later as `{"subnet": ["None found with id=N."]}`
# because MAAS scopes the subnet lookup to the interface's VLAN. Every mutation
# here is READ BACK and compared; a mismatch is fatal.
#
# Exit: 0 ok | 1 assertions failed | 2 refused (bad args / unproven region / no CLI)

set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/.." && pwd)"

ACTION=""; SITE=""; PROFILE=""; EXPECT_RACK=""; ONLY_HOST=""; COMMIT=0
while [ $# -gt 0 ]; do
  case "$1" in
    check|apply)   [ -z "$ACTION" ] && ACTION="$1" || SITE="$1"; shift ;;
    --profile)     PROFILE="${2:-}"; shift 2 ;;
    --expect-rack) EXPECT_RACK="${2:-}"; shift 2 ;;
    --host)        ONLY_HOST="${2:-}"; shift 2 ;;
    --commit)      COMMIT=1; shift ;;
    -h|--help)     ACTION=""; break ;;
    -*)            echo "FAIL: unknown option '$1'" >&2; exit 2 ;;
    *)             if [ -z "$ACTION" ]; then ACTION="$1"; elif [ -z "$SITE" ]; then SITE="$1"; else
                     echo "FAIL: unexpected argument '$1'" >&2; exit 2; fi; shift ;;
  esac
done

usage() {
  cat >&2 <<'U'
usage: dc-node-carve.sh <check|apply> <site> --profile <p> --expect-rack <r> [--host <h>] [--commit]
  e.g. dc-node-carve.sh check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid
       dc-node-carve.sh apply vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid \
                        --host vr1-dc0-storage-04 --commit

  check    assert the target carve, read-only. This is the NAMED GATE (GA-R6).
  apply    without --commit: resolve every id live and print the plan. Changes nothing.
           with --commit:    execute, reading back after every mutation.
  --host   restrict to ONE host. Use it: enlistment precedent says canary first,
           then batches of three.
U
  exit 2
}
[ -n "$ACTION" ] && [ -n "$SITE" ] || usage
case "$ACTION" in check|apply) ;; *) usage ;; esac

# --- no default profile ----------------------------------------------------
# The repo-wide default `MAAS_PROFILE=admin` resolves to the OFFICE1 region,
# where dc1's nine nodes still live. A 60-NIC re-home against the wrong region
# is either an idempotent no-op that prints PASS, or damage to the wrong DC.
[ -n "$PROFILE" ] || { echo "FAIL: --profile is REQUIRED. There is no default: 'admin' resolves to the OFFICE1 region." >&2; exit 2; }
[ -n "$EXPECT_RACK" ] || { echo "FAIL: --expect-rack is REQUIRED -- it is what proves the profile points at the region you mean." >&2; exit 2; }

# --- site refusal, symmetric to carve-host-interfaces.sh -------------------
# That script REFUSES vr1-* because it builds the VR0 Pattern-A VLAN-103 stack
# that D-133 supersedes. This one is its mirror image: it builds the D-133 FLAT
# per-NIC carve, which has no meaning on a VR0 fleet.
case "$SITE" in
  vr1-dc0|vr1-dc1) ;;
  *) echo "FAIL: site '$SITE' refused -- this script builds the D-133 FLAT per-NIC VR1 carve. VR0 hosts use scripts/carve-host-interfaces.sh (Pattern A + the VID-103 br-metal stack)." >&2; exit 2 ;;
esac

command -v maas >/dev/null 2>&1 || { echo "REFUSE: no 'maas' CLI on this host" >&2; exit 2; }
command -v python3 >/dev/null 2>&1 || { echo "REFUSE: no python3 on this host" >&2; exit 2; }

# --- region identity gate: BEFORE reading or writing anything --------------
ASSERT="$HERE/maas-profile-assert.sh"
[ -f "$ASSERT" ] || { echo "REFUSE: $ASSERT missing -- refusing to touch a region whose identity cannot be proven" >&2; exit 2; }
if ! bash "$ASSERT" "$PROFILE" "$EXPECT_RACK"; then
  echo "REFUSE: profile '$PROFILE' does not resolve to the expected region -- nothing was read or written" >&2
  exit 2
fi

# --- expected state: DERIVED from the libs, never typed --------------------
# shellcheck source=/dev/null
. "$REPO/scripts/lib-net.sh"
# shellcheck source=/dev/null
. "$REPO/scripts/lib-hosts.sh"
lib_net_select_dc   "$SITE" >/dev/null 2>&1 || { echo "FAIL: lib_net_select_dc rejected site '$SITE'" >&2; exit 2; }
lib_hosts_select_dc "$SITE" >/dev/null 2>&1 || { echo "FAIL: lib_hosts_select_dc rejected site '$SITE'" >&2; exit 2; }

P=0; F=0
ok(){   echo "  [ok]   $1"; P=$((P+1)); }
bad(){  echo "  [FAIL] $1"; F=$((F+1)); }
ck(){   if [ "$1" = 0 ]; then ok "$2"; else bad "$2"; fi; }
note(){ echo "  [note] $1"; }
hdr(){  echo; echo "=== $* ==="; }
refuse(){ echo "REFUSE: $1" >&2; exit 2; }

m(){ maas "$PROFILE" "$@"; }

# --- plane name -> CIDR, inverted from lib-net's own map -------------------
# PLANE_NAME is CIDR->name; NIC_PLANE_ORDER is by name. Invert rather than
# retype, so a plane re-address in lib-net.sh cannot silently disagree here.
declare -A CIDR_OF=()
for _c in "${PLANE_CIDRS[@]}"; do CIDR_OF["${PLANE_NAME[$_c]}"]="$_c"; done
for _p in "${NIC_PLANE_ORDER[@]}"; do
  [ -n "${CIDR_OF[$_p]:-}" ] || refuse "plane '$_p' from NIC_PLANE_ORDER has no CIDR in lib-net.sh PLANE_NAME for site $SITE"
done

# plane_ip <cidr> <octet> -- the D-134 host part is the SAME on every plane.
plane_ip(){ printf '%s.%s' "$(printf '%s' "${1%/*}" | cut -d. -f1-3)" "$2"; }
# nic_for <index0> -- macs[i] in the substrate root IS enp<i+1>s0 (measured on
# vr1-dc0-control-01 against the live carve).
nic_for(){ printf 'enp%ss0' "$(( $1 + 1 ))"; }

# --- live id resolution, never persisted -----------------------------------
SUBJSON=""; MACHJSON=""
reload_static() {
  SUBJSON="$(m subnets read 2>/dev/null)"   || refuse "could not read subnets"
  MACHJSON="$(m machines read 2>/dev/null)" || refuse "could not read machines"
  printf '%s' "$SUBJSON" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null || refuse "subnets payload is not JSON"
  printf '%s' "$MACHJSON" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null || refuse "machines payload is not JSON"
}

# sub_field <cidr> <id|vlan|gateway_ip> -- subnets keyed by CIDR, the stable key.
sub_field(){ printf '%s' "$SUBJSON" | python3 -c '
import json,sys
cidr,field=sys.argv[1],sys.argv[2]
for s in json.load(sys.stdin):
    if s.get("cidr")==cidr:
        if field=="vlan": print((s.get("vlan") or {}).get("id",""))
        elif field=="gateway_ip": print(s.get("gateway_ip") or "none")
        else: print(s.get(field,""))
        break
' "$1" "$2" 2>/dev/null; }

# mach_by_mac <boot-mac> <system_id|hostname|status_name> -- the ONLY stable key.
mach_by_mac(){ printf '%s' "$MACHJSON" | python3 -c '
import json,sys
mac,field=sys.argv[1].lower(),sys.argv[2]
for mm in json.load(sys.stdin):
    bi=mm.get("boot_interface") or {}
    if (bi.get("mac_address") or "").lower()==mac:
        print(mm.get(field,"")); break
' "$1" "$2" 2>/dev/null; }

IFJSON=""
reload_ifaces(){ IFJSON="$(m interfaces read "$1" 2>/dev/null)" || refuse "could not read interfaces for $1"; }

# if_field <name> <id|vlan|type|parents> from the loaded IFJSON
if_field(){ printf '%s' "$IFJSON" | python3 -c '
import json,sys
n,field=sys.argv[1],sys.argv[2]
for i in json.load(sys.stdin):
    if i.get("name")==n:
        if field=="vlan": print((i.get("vlan") or {}).get("id",""))
        elif field=="parents": print(",".join(i.get("parents") or []))
        else: print(i.get(field,""))
        break
' "$1" "$2" 2>/dev/null; }

# if_link <name> <cidr> -- "<mode>:<ip>" for the link on that subnet, else "".
if_link(){ printf '%s' "$IFJSON" | python3 -c '
import json,sys
n,cidr=sys.argv[1],sys.argv[2]
for i in json.load(sys.stdin):
    if i.get("name")==n:
        for l in (i.get("links") or []):
            s=l.get("subnet") or {}
            if s.get("cidr")==cidr:
                print("%s:%s"%(l.get("mode",""),l.get("ip_address") or "-")); sys.exit()
' "$1" "$2" 2>/dev/null; }

# if_link_id <name> <cidr> -- the MAAS link row id for that interface's link on
# that subnet, else "". Needed to unlink a commissioning AUTO link.
if_link_id(){ printf '%s' "$IFJSON" | python3 -c '
import json,sys
n,cidr=sys.argv[1],sys.argv[2]
for i in json.load(sys.stdin):
    if i.get("name")==n:
        for l in (i.get("links") or []):
            s=l.get("subnet") or {}
            if s.get("cidr")==cidr:
                print(l.get("id","")); sys.exit()
' "$1" "$2" 2>/dev/null; }

# if_l3_count <name> -- how many links carry a subnet at all (br-ex member must be 0).
if_l3_count(){ printf '%s' "$IFJSON" | python3 -c '
import json,sys
n=sys.argv[1]
for i in json.load(sys.stdin):
    if i.get("name")==n:
        print(sum(1 for l in (i.get("links") or []) if l.get("subnet"))); sys.exit()
print(0)
' "$1" 2>/dev/null; }

# if_names -- every interface name on the machine
if_names(){ printf '%s' "$IFJSON" | python3 -c '
import json,sys
for i in json.load(sys.stdin): print(i.get("name",""))
' 2>/dev/null; }

# --- mutation with mandatory read-back -------------------------------------
MUTATED=0
# do_update_vlan <sysid> <ifid> <vlanid> <nic> -- move a NIC onto a plane VLAN.
do_update_vlan(){
  local sid="$1" ifid="$2" vlan="$3" nic="$4" got
  if [ "$COMMIT" != 1 ]; then echo "  [plan] $nic(id=$ifid) -> VLAN $vlan"; return 0; fi
  m interface update "$sid" "$ifid" vlan="$vlan" >/dev/null 2>&1 || { bad "$nic: 'interface update vlan=$vlan' call failed"; return 1; }
  reload_ifaces "$sid"; got="$(if_field "$nic" vlan)"
  # READ BACK. MAAS has been measured returning 200 while changing nothing.
  if [ "$got" = "$vlan" ]; then ok "$nic -> VLAN $vlan (read back '$got')"; MUTATED=$((MUTATED+1)); return 0
  else bad "$nic: VLAN read back as '$got', wanted '$vlan' -- the call reported success and changed nothing"; return 1; fi
}
# do_link_static <sysid> <ifid> <subid> <ip> <nic> <cidr>
do_link_static(){
  local sid="$1" ifid="$2" sub="$3" ip="$4" nic="$5" cidr="$6" got
  if [ "$COMMIT" != 1 ]; then echo "  [plan] $nic(id=$ifid) -> STATIC $ip on subnet $sub ($cidr)"; return 0; fi
  m interface link-subnet "$sid" "$ifid" mode=STATIC subnet="$sub" ip_address="$ip" >/dev/null 2>&1 \
    || { bad "$nic: 'link-subnet STATIC $ip' call failed"; return 1; }
  reload_ifaces "$sid"; got="$(if_link "$nic" "$cidr")"
  if [ "$got" = "static:$ip" ]; then ok "$nic -> STATIC $ip on $cidr (read back '$got')"; MUTATED=$((MUTATED+1)); return 0
  else bad "$nic: link read back as '$got', wanted 'static:$ip'"; return 1; fi
}
# do_unlink <sysid> <ifid> <linkid> <nic> <reason>
# The REASON is a parameter, not a constant: this is called for two different
# reasons (moving a member's L3 onto br-ex, and converting a commissioning auto
# link to STATIC) and a hardcoded one made the plan for the metal-admin leg read
# "L3 moves to br-ex", which is false. A mutation log that states the wrong
# reason is the confident-but-wrong evidence class this repo keeps paying for.
do_unlink(){
  local sid="$1" ifid="$2" lid="$3" nic="$4" why="${5:-}"
  if [ "$COMMIT" != 1 ]; then echo "  [plan] unlink $nic(id=$ifid) link id=$lid ($why)"; return 0; fi
  m interface unlink-subnet "$sid" "$ifid" id="$lid" >/dev/null 2>&1 || { bad "$nic: unlink link id=$lid failed"; return 1; }
  reload_ifaces "$sid"; ok "$nic: unlinked id=$lid (now $(if_l3_count "$nic") L3 link(s))"; MUTATED=$((MUTATED+1))
}

# --- one raw plane leg: VLAN move then STATIC ------------------------------
carve_raw(){
  local sid="$1" nic="$2" cidr="$3" ip="$4" ifid vlan sub cur link lid
  ifid="$(if_field "$nic" id)"
  [ -n "$ifid" ] || { bad "$nic not found on this machine"; return 1; }
  vlan="$(sub_field "$cidr" vlan)"; sub="$(sub_field "$cidr" id)"
  [ -n "$vlan" ] && [ -n "$sub" ] || { bad "$nic: no VLAN/subnet resolved for $cidr in this region -- REFUSING to guess"; return 1; }

  link="$(if_link "$nic" "$cidr")"
  if [ "$link" = "static:$ip" ]; then note "$nic already STATIC $ip on $cidr -- SKIP"; ok "$nic STATIC $ip on $cidr"; return 0; fi

  # A COMMISSIONING LINK IS THE EXPECTED STARTING STATE, NOT A CONFLICT. Every
  # PXE leg comes out of enlistment holding `auto` on metal-admin (measured: all
  # ten dc0 machines). MAAS will not convert a link's mode in place, so the auto
  # link is UNLINKED and re-created as STATIC. This is the one case where an
  # existing link on the target subnet is replaced rather than refused -- and it
  # is narrow BY MODE: an `auto`/`dhcp`/`link_up` link is MAAS's own default, a
  # `static` one was put there by a human or an earlier ruled carve.
  case "$link" in
    auto:*|dhcp:*|link_up:*)
      lid="$(if_link_id "$nic" "$cidr")"
      if [ -n "$lid" ]; then
        note "$nic holds a commissioning '${link%%:*}' link on $cidr -- converting to STATIC $ip"
        do_unlink "$sid" "$ifid" "$lid" "$nic" "converting the commissioning ${link%%:*} link to STATIC" || return 1
        ifid="$(if_field "$nic" id)"
      fi
      ;;
    "") ;;
    *)
      # A STATIC link on the RIGHT subnet with the WRONG address is a FAILURE,
      # never a skip: something deliberate put it there and silently moving it
      # could strand whatever depends on it.
      bad "$nic: already linked to $cidr as '$link' but the ruled address is '$ip' -- refusing to silently accept it"; return 1
      ;;
  esac

  cur="$(if_field "$nic" vlan)"
  if [ "$cur" != "$vlan" ]; then do_update_vlan "$sid" "$ifid" "$vlan" "$nic" || return 1
  else note "$nic already on VLAN $vlan -- SKIP move"; fi
  ifid="$(if_field "$nic" id)"
  do_link_static "$sid" "$ifid" "$sub" "$ip" "$nic" "$cidr"
}

# --- Pattern A: the OVS br-ex on the provider uplink -----------------------
# GUARD STRUCTURE IS DELIBERATE AND MIRRORS carve-host-interfaces.sh:211-236.
# The VLAN move sits INSIDE the "br-ex does not exist yet" branch: a MAAS bridge
# inherits its parent's VLAN AT CREATE, so the parent must be moved first or the
# br-ex static lands on the wrong plane -- and once br-ex exists the parent is
# ENSLAVED, where a VLAN update is invalid. Restructuring this for symmetry with
# carve_raw breaks re-runnability.
build_ovs_brex(){
  local sid="$1" nic="$2" cidr="$3" ip="$4" ifid brid vlan sub lid link
  ifid="$(if_field "$nic" id)"
  [ -n "$ifid" ] || { bad "$nic not found on this machine"; return 1; }
  vlan="$(sub_field "$cidr" vlan)"; sub="$(sub_field "$cidr" id)"
  [ -n "$vlan" ] && [ -n "$sub" ] || { bad "br-ex: no VLAN/subnet resolved for $cidr -- REFUSING to guess"; return 1; }

  # 1) clear the member's commissioning link(s) so the host L3 lands on br-ex.
  for lid in $(printf '%s' "$IFJSON" | python3 -c '
import json,sys
n=sys.argv[1]
for i in json.load(sys.stdin):
    if i.get("name")==n:
        for l in (i.get("links") or []):
            if l.get("subnet"): print(l.get("id"))
' "$nic" 2>/dev/null); do
    do_unlink "$sid" "$ifid" "$lid" "$nic" "L3 moves to br-ex" || return 1
  done

  # 2) create the OVS bridge, moving the parent onto the provider VLAN FIRST.
  brid="$(if_field br-ex id)"
  if [ -z "$brid" ]; then
    if [ "$(if_field "$nic" vlan)" != "$vlan" ]; then do_update_vlan "$sid" "$ifid" "$vlan" "$nic" || return 1; fi
    ifid="$(if_field "$nic" id)"
    if [ "$COMMIT" != 1 ]; then echo "  [plan] create br-ex (OVS) parent=$nic(id=$ifid)"
    else
      m interfaces create-bridge "$sid" name=br-ex bridge_type=ovs parent="$ifid" >/dev/null 2>&1 \
        || { bad "br-ex: create-bridge failed"; return 1; }
      reload_ifaces "$sid"; brid="$(if_field br-ex id)"
      if [ -n "$brid" ] && [ "$(if_field br-ex type)" = "bridge" ]; then
        ok "br-ex created (OVS) parent=$nic, read back type='bridge' parents='$(if_field br-ex parents)'"; MUTATED=$((MUTATED+1))
      else bad "br-ex: not present after create-bridge reported success"; return 1; fi
    fi
  else note "br-ex exists -- SKIP create"; fi

  # 3) the host static lands on br-ex, not on the member NIC.
  [ "$COMMIT" = 1 ] && brid="$(if_field br-ex id)" || brid="${brid:-<br-ex-id>}"
  link="$(if_link br-ex "$cidr")"
  if [ "$link" = "static:$ip" ]; then note "br-ex already STATIC $ip on $cidr -- SKIP"; ok "br-ex STATIC $ip on $cidr"; return 0; fi
  if [ -n "$link" ]; then bad "br-ex: linked to $cidr as '$link' but the ruled address is '$ip'"; return 1; fi
  do_link_static "$sid" "$brid" "$sub" "$ip" "br-ex" "$cidr"
}

# --- role resolution -- DERIVED from the host token, never typed -----------
is_juju_host(){ case "$1" in *"$JUJU_HOST_SUFFIX") return 0 ;; *) return 1 ;; esac; }
planes_for_host(){ if is_juju_host "$1"; then echo "$NODE_CARVE_JUJU_PLANES"; else echo "$NODE_CARVE_ROLE_PLANES"; fi; }

# ===========================================================================
reload_static

PROV_CIDR="${CIDR_OF[provider-public]}"
MA_CIDR="${CIDR_OF[metal-admin]}"

hdr "region + plane preconditions ($SITE via profile '$PROFILE')"
GW="$(sub_field "$PROV_CIDR" gateway_ip)"
# THE DEFAULT ROUTE IS A NODE-SIDE FACT AND IS NOT OBSERVABLE FROM HERE. What
# this gate can prove is the MODEL that produces it: a static link on
# provider-public plus a gateway on that subnet. The routing table itself is a
# FIRST-BOOT observation (gate G17), not something a green carve establishes.
ck "$([ "$GW" = "10.12.4.1" ] || [ "$GW" = "10.12.64.1" ] && echo 0 || echo 1)" \
   "provider-public $PROV_CIDR carries a gateway (got '$GW') -- the node default route depends on it"
ck "$([ "$(sub_field "$MA_CIDR" gateway_ip)" = "none" ] && echo 0 || echo 1)" \
   "metal-admin $MA_CIDR has NO gateway (got '$(sub_field "$MA_CIDR" gateway_ip)') -- D-134/D-125: it is not an egress plane"

HOSTS_TODO=()
if [ -n "$ONLY_HOST" ]; then
  _found=0; for h in "${HOSTS[@]}"; do [ "$h" = "$ONLY_HOST" ] && _found=1; done
  [ "$_found" = 1 ] || refuse "--host '$ONLY_HOST' is not a $SITE host in lib-hosts.sh HOSTS"
  HOSTS_TODO=( "$ONLY_HOST" )
else
  HOSTS_TODO=( "${HOSTS[@]}" )
fi

for HOST in "${HOSTS_TODO[@]}"; do
  MAC="${HOST_BOOT_MAC[$HOST]:-}"; OCTET="${HOST_OCTET[$HOST]:-}"
  [ -n "$MAC" ] && [ -n "$OCTET" ] || { bad "$HOST: no pinned boot MAC / octet in lib-hosts.sh"; continue; }
  SID="$(mach_by_mac "$MAC" system_id)"
  if [ -z "$SID" ]; then bad "$HOST: pinned boot MAC $MAC has NO machine in this region -- not enrolled here"; continue; fi
  HN="$(mach_by_mac "$MAC" hostname)"; ST="$(mach_by_mac "$MAC" status_name)"
  NPLANES="$(planes_for_host "$HOST")"
  hdr "$HOST -> $SID/$HN (octet $OCTET, $NPLANES plane(s), status $ST)"

  # MAAS REFUSES interface changes on a Deployed machine. Assert before mutating
  # rather than discovering it call-by-call.
  if [ "$ACTION" = apply ] && [ "$ST" != "Ready" ]; then
    bad "$HOST is '$ST', not 'Ready' -- MAAS rejects interface changes on a Deployed machine; skipping"
    continue
  fi
  reload_ifaces "$SID"

  IDX=0
  for PLANE in "${NIC_PLANE_ORDER[@]}"; do
    [ "$IDX" -lt "$NPLANES" ] || break
    NIC="$(nic_for "$IDX")"; CIDR="${CIDR_OF[$PLANE]}"; IP="$(plane_ip "$CIDR" "$OCTET")"
    if [ "$ACTION" = check ]; then
      if [ "$PLANE" = provider-public ] && ! is_juju_host "$HOST"; then
        ck "$([ "$(if_link br-ex "$CIDR")" = "static:$IP" ] && echo 0 || echo 1)" "$HOST br-ex STATIC $IP on $PLANE (got '$(if_link br-ex "$CIDR")')"
        ck "$([ "$(if_field br-ex type)" = "bridge" ] && echo 0 || echo 1)" "$HOST br-ex is a bridge (got '$(if_field br-ex type)')"
        ck "$([ "$(if_field br-ex parents)" = "$NIC" ] && echo 0 || echo 1)" "$HOST br-ex parent is $NIC (got '$(if_field br-ex parents)')"
        # The member NIC must hold NO L3 link, or the host has two addresses on
        # one plane and OVS competes with the raw NIC (the D-057 Pattern B fault
        # that darkened the floating-IP plane).
        ck "$([ "$(if_l3_count "$NIC")" = "0" ] && echo 0 || echo 1)" "$HOST $NIC holds no L3 link (got $(if_l3_count "$NIC")) -- it is an OVS member"
      else
        ck "$([ "$(if_link "$NIC" "$CIDR")" = "static:$IP" ] && echo 0 || echo 1)" "$HOST $NIC STATIC $IP on $PLANE (got '$(if_link "$NIC" "$CIDR")')"
        ck "$([ "$(if_field "$NIC" vlan)" = "$(sub_field "$CIDR" vlan)" ] && echo 0 || echo 1)" "$HOST $NIC on the $PLANE VLAN (got '$(if_field "$NIC" vlan)' want '$(sub_field "$CIDR" vlan)')"
      fi
    else
      if [ "$PLANE" = provider-public ] && ! is_juju_host "$HOST"; then
        build_ovs_brex "$SID" "$NIC" "$CIDR" "$IP" || true
      else
        carve_raw "$SID" "$NIC" "$CIDR" "$IP" || true
      fi
    fi
    IDX=$((IDX+1))
  done

  # The controller's enp3s0..enp6s0 are DELIBERATELY left on auto VLANs. Assert
  # that deliberate absence, so a later session cannot "helpfully" carve them
  # and have every gate stay green.
  if [ "$ACTION" = check ] && is_juju_host "$HOST"; then
    _stray=0
    for _i in $(seq "$NPLANES" 5); do
      _n="$(nic_for "$_i")"
      for _p in "${NIC_PLANE_ORDER[@]}"; do
        [ -n "$(if_link "$_n" "${CIDR_OF[$_p]}")" ] && _stray=$((_stray+1))
      done
    done
    ck "$([ "$_stray" = 0 ] && echo 0 || echo 1)" "$HOST enp$((NPLANES+1))s0..enp6s0 carry NO plane link ($_stray found) -- deliberately left on auto VLANs"
    ck "$([ -z "$(if_field br-ex id)" ] && echo 0 || echo 1)" "$HOST has NO br-ex (got id '$(if_field br-ex id)') -- the controller takes the provider leg RAW"
  fi
done

echo
if [ "$ACTION" = apply ] && [ "$COMMIT" != 1 ]; then
  echo "PLAN ONLY -- nothing was changed. Re-run with --commit to execute."
fi
[ "$COMMIT" = 1 ] && echo "mutations applied (each read back): $MUTATED"
echo "NOTE: this gate proves the MAAS MODEL. The node's actual routing table is a"
echo "      FIRST-BOOT observation (G17) -- a green carve is not a proven default route."
echo "RESULT: pass=$P fail=$F"
if [ "$F" -gt 0 ]; then echo "FAIL: dc-node-carve $ACTION $SITE"; exit 1; fi
echo "PASS: dc-node-carve $ACTION $SITE"
exit 0
