#!/usr/bin/env bash
# tests/dc-node-carve/run-tests.sh -- unit tests for scripts/dc-node-carve.sh
# (2026-07-30).
#
# A stub `maas` on PATH serves JSON fixtures, so no live region is touched.
# STUB_STATE selects a fixture set:
# raw the MEASURED starting state of every migrated dc0 machine:
# enp1s0 with an AUTO link on metal-admin, enp2s0..enp6s0 each on a
# FRESH auto-created fabric with `link_up` and no subnet.
# carved the intended end state: 9 role nodes on all six planes with an
# OVS br-ex, the juju controller on two planes raw.
# wrongip carved, except one node's storage leg holds the WRONG address on
# the RIGHT subnet. This MUST fail, never skip.
# patternb carved, except a role node's provider static sits on the raw
# enp2s0 with no br-ex -- the D-057 Pattern B shape that darkened
# the floating-IP plane.
# memberl3 carved, except the br-ex MEMBER NIC still holds an L3 link, so
# the host has two addresses on the provider plane.
# brexip carved, and br-ex is a correctly-parented OVS bridge, but its
# static is the WRONG address. ADDED after a mutation pass found
# the br-ex static compare could be neutered with the suite still
# green: `patternb` was being caught by the type/parent assertions
# instead, so the address compare itself was never exercised.
# jujucarved carved, except the juju controller's enp3s0 has been "helpfully"
# carved onto a plane it is deliberately meant to leave alone.
# jujubrex carved, except the juju controller has grown a br-ex.
# nogw carved, except provider-public lost its gateway_ip -- the model
# fact the node default route depends on.
#
# THE POINT OF THIS HARNESS is that `check` must FAIL on every one of those
# partial or wrong states. The repo's receipts are three-for-three that freshly
# written assertions are decoration until each is proven able to fail, so every
# assertion added here has a failing-direction fixture above.
#
# It also pins the REGION GATE (no default profile; refuse when the profile does
# not resolve to the expected rack) and the SITE refusal, which is the mirror
# image of carve-host-interfaces.sh refusing vr1-*.
set -uo pipefail
SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SD/../.." && pwd)"
SCRIPT="$ROOT/scripts/dc-node-carve.sh"
P=0; F=0
ok(){ echo "PASS: $1"; P=$((P+1)); }
no(){ echo "FAIL: $1"; F=$((F+1)); }
chk(){ [ "$2" = "$3" ] && ok "$1" || no "$1 (got '$2' want '$3')"; }
has(){ printf '%s' "$2" | grep -q -- "$3" && ok "$1" || no "$1 (output lacked '$3')"; }
hasnt(){ printf '%s' "$2" | grep -q -- "$3" && no "$1 (output unexpectedly had '$3')" || ok "$1"; }
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
mkdir -p "$TMP/bin"
cat > "$TMP/bin/maas" <<'STUB'
#!/usr/bin/env bash
prof="$1"; shift
sub="$1"; shift
S="${STUB_STATE:-carved}"
# --- the ten dc0 machines, keyed by their PINNED boot MACs (lib-hosts.sh) ----
# NOTE: this heredoc is DELIBERATELY unquoted -- STUB_S04_STATUS must expand.
# A quoted <<'M' leaves the literal ${...} in place and the payload stops being
# JSON, which is the same invisible-heredoc class as finding F8.
emit_machines() {
cat <<M
[{"system_id":"c01","hostname":"mint-roughy","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:be:69:c5"}},
{"system_id":"c02","hostname":"gentle-raven","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:02:ff:57"}},
{"system_id":"c03","hostname":"square-insect","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:4f:de:a9"}},
{"system_id":"m01","hostname":"able-puma","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:1b:19:e6"}},
{"system_id":"m02","hostname":"pure-condor","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:18:ab:b4"}},
{"system_id":"s01","hostname":"amused-corgi","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:5f:8d:42"}},
{"system_id":"s02","hostname":"known-alpaca","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:48:86:2c"}},
{"system_id":"s03","hostname":"loyal-oyster","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:b1:94:d1"}},
{"system_id":"s04","hostname":"civil-bug","status_name":"${STUB_S04_STATUS:-Ready}","boot_interface":{"mac_address":"52:54:00:2b:ed:ab"}},
{"system_id":"j01","hostname":"subtle-grouse","status_name":"Ready","boot_interface":{"mac_address":"52:54:00:48:e7:1e"}}]
M
}
emit_subnets() {
local pgw='"10.12.4.1"'
[ "$S" = nogw ] && pgw='null'
cat <<S2
[{"id":1,"cidr":"10.12.8.0/22","gateway_ip":null,"vlan":{"id":5001,"fabric":"fabric-0"}},
{"id":3,"cidr":"10.12.4.0/22","gateway_ip":$pgw,"vlan":{"id":5002,"fabric":"vr1-dc0-provider-public"}},
{"id":4,"cidr":"10.12.12.0/22","gateway_ip":null,"vlan":{"id":5004,"fabric":"vr1-dc0-metal-internal"}},
{"id":5,"cidr":"10.12.16.0/22","gateway_ip":null,"vlan":{"id":5005,"fabric":"vr1-dc0-data-tenant"}},
{"id":6,"cidr":"10.12.32.0/22","gateway_ip":null,"vlan":{"id":5006,"fabric":"vr1-dc0-storage"}},
{"id":7,"cidr":"10.12.36.0/22","gateway_ip":null,"vlan":{"id":5007,"fabric":"vr1-dc0-replication"}}]
S2
}
# octet for a system_id -- the D-134 bands
octet_of() {
case "$1" in
c01) echo 100 ;; c02) echo 101 ;; c03) echo 102 ;;
m01) echo 120 ;; m02) echo 121 ;;
s01) echo 150 ;; s02) echo 151 ;; s03) echo 152 ;; s04) echo 153 ;;
j01) echo 5 ;;
esac
}
# a raw, uncarved machine: enp1s0 auto on metal-admin, enp2-6 link_up on autofabs
emit_raw() {
cat <<R
[{"id":1,"name":"enp1s0","type":"physical","vlan":{"id":5001},"parents":[],"links":[{"id":11,"mode":"auto","ip_address":null,"subnet":{"cidr":"10.12.8.0/22"}}]},
{"id":2,"name":"enp2s0","type":"physical","vlan":{"id":5100},"parents":[],"links":[{"id":12,"mode":"link_up","ip_address":null,"subnet":null}]},
{"id":3,"name":"enp3s0","type":"physical","vlan":{"id":5101},"parents":[],"links":[{"id":13,"mode":"link_up","ip_address":null,"subnet":null}]},
{"id":4,"name":"enp4s0","type":"physical","vlan":{"id":5102},"parents":[],"links":[{"id":14,"mode":"link_up","ip_address":null,"subnet":null}]},
{"id":5,"name":"enp5s0","type":"physical","vlan":{"id":5103},"parents":[],"links":[{"id":15,"mode":"link_up","ip_address":null,"subnet":null}]},
{"id":6,"name":"enp6s0","type":"physical","vlan":{"id":5104},"parents":[],"links":[{"id":16,"mode":"link_up","ip_address":null,"subnet":null}]}]
R
}
# a fully carved ROLE node
emit_role() {
local N="$1" stor_ip brex_blk enp2_links
stor_ip="10.12.32.$N"
[ "$S" = wrongip ] && [ "$1" = 153 ] && stor_ip="10.12.32.199"
enp2_links='[]'
local brex_ip="10.12.4.$N"
[ "$S" = brexip ] && [ "$N" = 150 ] && brex_ip="10.12.4.199"
brex_blk='{"id":90,"name":"br-ex","type":"bridge","vlan":{"id":5002},"parents":["enp2s0"],"links":[{"id":90,"mode":"static","ip_address":"'"$brex_ip"'","subnet":{"cidr":"10.12.4.0/22"}}]},'
if [ "$S" = patternb ] && [ "$N" = 150 ]; then
# provider static on the RAW nic, no bridge at all
brex_blk=''
enp2_links='[{"id":21,"mode":"static","ip_address":"10.12.4.'"$N"'","subnet":{"cidr":"10.12.4.0/22"}}]'
fi
if [ "$S" = memberl3 ] && [ "$N" = 151 ]; then
# bridge present, but the member NIC kept an L3 link too
enp2_links='[{"id":22,"mode":"static","ip_address":"10.12.4.'"$N"'","subnet":{"cidr":"10.12.4.0/22"}}]'
fi
cat <<RO
[$brex_blk
{"id":1,"name":"enp1s0","type":"physical","vlan":{"id":5001},"parents":[],"links":[{"id":1,"mode":"static","ip_address":"10.12.8.$N","subnet":{"cidr":"10.12.8.0/22"}}]},
{"id":2,"name":"enp2s0","type":"physical","vlan":{"id":5002},"parents":[],"links":$enp2_links},
{"id":3,"name":"enp3s0","type":"physical","vlan":{"id":5004},"parents":[],"links":[{"id":3,"mode":"static","ip_address":"10.12.12.$N","subnet":{"cidr":"10.12.12.0/22"}}]},
{"id":4,"name":"enp4s0","type":"physical","vlan":{"id":5005},"parents":[],"links":[{"id":4,"mode":"static","ip_address":"10.12.16.$N","subnet":{"cidr":"10.12.16.0/22"}}]},
{"id":5,"name":"enp5s0","type":"physical","vlan":{"id":5006},"parents":[],"links":[{"id":5,"mode":"static","ip_address":"$stor_ip","subnet":{"cidr":"10.12.32.0/22"}}]},
{"id":6,"name":"enp6s0","type":"physical","vlan":{"id":5007},"parents":[],"links":[{"id":6,"mode":"static","ip_address":"10.12.36.$N","subnet":{"cidr":"10.12.36.0/22"}}]}]
RO
}
# the JUJU CONTROLLER: two planes RAW, no br-ex, enp3-6 left on auto VLANs
emit_juju() {
local extra='' brex=''
if [ "$S" = jujucarved ]; then
extra='{"id":3,"name":"enp3s0","type":"physical","vlan":{"id":5004},"parents":[],"links":[{"id":3,"mode":"static","ip_address":"10.12.12.5","subnet":{"cidr":"10.12.12.0/22"}}]},'
else
extra='{"id":3,"name":"enp3s0","type":"physical","vlan":{"id":5201},"parents":[],"links":[{"id":3,"mode":"link_up","ip_address":null,"subnet":null}]},'
fi
[ "$S" = jujubrex ] && brex='{"id":90,"name":"br-ex","type":"bridge","vlan":{"id":5002},"parents":["enp2s0"],"links":[]},'
cat <<JU
[$brex
{"id":1,"name":"enp1s0","type":"physical","vlan":{"id":5001},"parents":[],"links":[{"id":1,"mode":"static","ip_address":"10.12.8.5","subnet":{"cidr":"10.12.8.0/22"}}]},
{"id":2,"name":"enp2s0","type":"physical","vlan":{"id":5002},"parents":[],"links":[{"id":2,"mode":"static","ip_address":"10.12.4.5","subnet":{"cidr":"10.12.4.0/22"}}]},
$extra
{"id":4,"name":"enp4s0","type":"physical","vlan":{"id":5202},"parents":[],"links":[{"id":4,"mode":"link_up","ip_address":null,"subnet":null}]},
{"id":5,"name":"enp5s0","type":"physical","vlan":{"id":5203},"parents":[],"links":[{"id":5,"mode":"link_up","ip_address":null,"subnet":null}]},
{"id":6,"name":"enp6s0","type":"physical","vlan":{"id":5204},"parents":[],"links":[{"id":6,"mode":"link_up","ip_address":null,"subnet":null}]}]
JU
}
case "$sub" in
rack-controllers)
case "${STUB_RACK:-hot-kid}" in
none) echo '[]' ;;
*) printf '[{"hostname":"%s"}]\n' "${STUB_RACK:-hot-kid}" ;;
esac ;;
machines) emit_machines ;;
machine) emit_machines ;;
subnets) emit_subnets ;;
interfaces)
act="${1:-read}"; sid="${2:-}"
# `interfaces read <sysid>` -> $1=read $2=sysid ; create-bridge -> $1=create-bridge $2=sysid
if [ "$act" = "create-bridge" ]; then echo '{"id":90}'; exit 0; fi
if [ "$S" = raw ]; then emit_raw; exit 0; fi
case "$sid" in
j01) emit_juju ;;
*) emit_role "$(octet_of "$sid")" ;;
esac ;;
interface) echo '{}' ;;
*) echo '[]' ;;
esac
STUB
chmod +x "$TMP/bin/maas"
export PATH="$TMP/bin:$PATH"
R(){ STUB_STATE="$1" bash "$SCRIPT" "${@:2}" 2>&1; }
GATE=(--profile vr1-dc0-region --expect-rack hot-kid)
echo "== T1..T6 argument + region + site gates =="
out="$(bash "$SCRIPT" check vr1-dc0 --expect-rack hot-kid 2>&1)"; rc=$?
chk "T1 missing --profile refuses rc=2" "$rc" "2"
has "T1b names the OFFICE1 hazard" "$out" "OFFICE1"
out="$(bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region 2>&1)"; rc=$?
chk "T2 missing --expect-rack refuses rc=2" "$rc" "2"
out="$(bash "$SCRIPT" check vr0-dc0 "${GATE[@]}" 2>&1)"; rc=$?
chk "T3 vr0 site refused rc=2" "$rc" "2"
has "T3b points at carve-host-interfaces.sh" "$out" "carve-host-interfaces.sh"
out="$(STUB_RACK=wrong-rack bash "$SCRIPT" check vr1-dc0 "${GATE[@]}" 2>&1)"; rc=$?
chk "T4 wrong region refuses rc=2" "$rc" "2"
hasnt "T4b nothing was read after the refusal" "$out" "plane preconditions"
out="$(STUB_RACK=none bash "$SCRIPT" check vr1-dc0 "${GATE[@]}" 2>&1)"; rc=$?
chk "T5 region with no racks refuses rc=2" "$rc" "2"
out="$(R carved check vr1-dc0 "${GATE[@]}" --host not-a-host)"; rc=$?
chk "T6 unknown --host refuses rc=2" "$rc" "2"
echo
echo "== T7..T9 the intended end state PASSES =="
out="$(R carved check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T7 carved state passes rc=0" "$rc" "0"
has "T7b reports fail=0" "$out" "fail=0"
has "T8 asserts br-ex parent" "$out" "br-ex parent is enp2s0"
has "T8b asserts the member holds no L3" "$out" "holds no L3 link"
has "T9 asserts the controller has NO br-ex" "$out" "has NO br-ex"
has "T9b asserts the controller's spare NICs are left alone" "$out" "deliberately left on auto VLANs"
has "T9c states the routing table is a first-boot fact" "$out" "FIRST-BOOT observation"
echo
echo "== T10..T15 every wrong state must FAIL (the mutation-proof set) =="
out="$(R raw check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T10 the measured uncarved state FAILS rc=1" "$rc" "1"
out="$(R wrongip check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T11 right subnet + WRONG address FAILS rc=1" "$rc" "1"
has "T11b names the wrong address" "$out" "10.12.32.199"
out="$(R patternb check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T12 Pattern B (static on raw enp2s0, no br-ex) FAILS rc=1" "$rc" "1"
out="$(R memberl3 check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T13 br-ex member still holding an L3 link FAILS rc=1" "$rc" "1"
out="$(R jujucarved check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T14 controller's spare NIC carved FAILS rc=1" "$rc" "1"
out="$(R jujubrex check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T15 controller grown a br-ex FAILS rc=1" "$rc" "1"
out="$(R brexip check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T15c br-ex with the WRONG static FAILS rc=1" "$rc" "1"
has "T15d names the wrong br-ex address" "$out" "10.12.4.199"
out="$(R nogw check vr1-dc0 "${GATE[@]}")"; rc=$?
chk "T16 provider-public with no gateway FAILS rc=1" "$rc" "1"
has "T16b explains the default route depends on it" "$out" "default route"
echo
echo "== T17..T20 role asymmetry and NIC order are DERIVED, not typed =="
out="$(R carved check vr1-dc0 "${GATE[@]}" --host vr1-dc0-juju-01)"; rc=$?
chk "T17 controller alone passes rc=0" "$rc" "0"
has "T17b controller carved on 2 planes" "$out" "2 plane(s)"
hasnt "T17c controller is NOT given a data-tenant leg" "$out" "enp4s0 STATIC 10.12.16"
out="$(R carved check vr1-dc0 "${GATE[@]}" --host vr1-dc0-control-01)"; rc=$?
chk "T18 role node alone passes rc=0" "$rc" "0"
has "T18b role node carved on 6 planes" "$out" "6 plane(s)"
# enp1s0 is METAL-ADMIN, not provider-public. Walking lib-net.sh PLANE_CIDRS
# positionally would put PXE on provider-public and strand commissioning.
has "T19 enp1s0 is metal-admin (NIC_PLANE_ORDER, not PLANE_CIDRS)" "$out" "enp1s0 STATIC 10.12.8.100 on metal-admin"
has "T19b enp2s0/br-ex is provider-public" "$out" "br-ex STATIC 10.12.4.100 on provider-public"
echo
echo "== T21..T24 apply is DRY-RUN without --commit =="
out="$(R raw apply vr1-dc0 "${GATE[@]}" --host vr1-dc0-storage-04)"; rc=$?
has "T21 plan-only banner" "$out" "PLAN ONLY -- nothing was changed"
has "T22 plans the VLAN move" "$out" "\[plan\] enp3s0"
has "T23 plans the br-ex create" "$out" "\[plan\] create br-ex (OVS) parent=enp2s0"
hasnt "T24 dry-run reports no mutations" "$out" "mutations applied"
# THE COMMISSIONING LINK IS THE EXPECTED STARTING STATE, NOT A CONFLICT. Every
# PXE leg comes out of enlistment holding `auto` on metal-admin. A live dry-run
# caught this: the tool refused enp1s0 outright, so it could not carve the
# metal-admin leg on ANY of the ten machines.
chk "T26 apply over the measured raw state plans cleanly rc=0" "$rc" "0"
has "T26b names the commissioning link it is converting" "$out" "holds a commissioning 'auto' link"
has "T26c plans the unlink before the static" "$out" "\[plan\] unlink enp1s0"
has "T26d plans the metal-admin static" "$out" "\[plan\] enp1s0(id=1) -> STATIC 10.12.8.153"
hasnt "T26e does NOT refuse the auto link" "$out" "refusing to silently accept it"
# The auto-link conversion above must NOT have widened into "overwrite anything".
# A STATIC link on the right subnet with the wrong address was put there
# deliberately; moving it silently could strand whatever depends on it.
out="$(R wrongip apply vr1-dc0 "${GATE[@]}" --host vr1-dc0-storage-04)"; rc=$?
chk "T27 apply still REFUSES a static wrong address rc=1" "$rc" "1"
has "T27b names the refusal" "$out" "refusing to silently accept it"
has "T27c names the conflicting address" "$out" "10.12.32.199"
out="$(STUB_S04_STATUS=Deployed R carved apply vr1-dc0 "${GATE[@]}" --host vr1-dc0-storage-04 --commit)"; rc=$?
chk "T25 apply on a Deployed machine FAILS rc=1" "$rc" "1"
has "T25b explains MAAS rejects it" "$out" "rejects interface changes"
echo
echo "RESULT: $P passed, $F failed"
[ "$F" -eq 0 ] || exit 1