#!/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>
do_unlink(){
local sid="$1" ifid="$2" lid="$3" nic="$4"
if [ "$COMMIT" != 1 ]; then echo " [plan] unlink $nic(id=$ifid) link id=$lid (L3 moves to br-ex)"; 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" || 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" || 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