#!/usr/bin/env bash
# tests/dc-region-topology/run-tests.sh -- unit tests for
# scripts/dc-region-topology.sh (2026-07-30).
#
# A stub `maas` on PATH serves JSON fixtures, so no live region is touched.
# STUB_STATE selects a fixture set:
# empty a freshly `maas init`ed region: two auto fabrics from the region
# VM's own NICs, no named fabrics, no spaces, 3 subnets. This is the
# MEASURED starting state of vr1-dc0-maas-01.
# complete the intended end state.
# wrongfab complete, except provider-public's subnet still sits on an auto
# fabric -- the case that needs a MOVE, not a create.
# nospace complete, except one VLAN is not bound to its space.
#
# FIXTURE FIDELITY: an unbound VLAN reads as the STRING "undefined" in MAAS's
# JSON, not as null. The first version of these fixtures used null and passed --
# graded against a payload MAAS never emits. Corrected after a live read of the
# dc0 region, because a harness whose fixtures do not match reality tests the
# wrong program.
#
# THE POINT OF THIS HARNESS is that `check` must FAIL on every partial state.
# A topology gate that passes on a half-built region is worse than none: the
# migration's whole premise is that the new region is provably equivalent
# before ten machines are deleted from the old one.
#
# It also pins the REGION GATE: the script must refuse entirely when the profile
# does not resolve to the expected rack, and must refuse when --profile or
# --expect-rack is omitted. Defaulting the profile to 'admin' would point this
# at the OFFICE1 region, where `apply` is an idempotent no-op that prints PASS.
set -uo pipefail
SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SD/../.." && pwd)"
SCRIPT="$ROOT/scripts/dc-region-topology.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')"; }
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
mkdir -p "$TMP/bin"
cat > "$TMP/bin/maas" <<'STUB'
#!/usr/bin/env bash
# $1 = profile, $2.. = command
prof="$1"; shift
sub="$1"; shift
S="${STUB_STATE:-empty}"
named='"vr1-dc0-provider-public" "vr1-dc0-metal-internal" "vr1-dc0-data-tenant" "vr1-dc0-storage" "vr1-dc0-replication"'
emit_fabrics() {
case "$S" in
empty)
echo '[{"id":0,"name":"fabric-0","vlans":[{"id":5001,"vid":0}]},{"id":1,"name":"fabric-1","vlans":[{"id":5002,"vid":0}]}]' ;;
*)
echo '[{"id":0,"name":"fabric-0","vlans":[{"id":5001,"vid":0}]},
{"id":1,"name":"fabric-1","vlans":[{"id":5002,"vid":0}]},
{"id":2,"name":"vr1-dc0-provider-public","vlans":[{"id":5189,"vid":0}]},
{"id":3,"name":"vr1-dc0-metal-internal","vlans":[{"id":5190,"vid":0}]},
{"id":4,"name":"vr1-dc0-data-tenant","vlans":[{"id":5191,"vid":0}]},
{"id":5,"name":"vr1-dc0-storage","vlans":[{"id":5192,"vid":0}]},
{"id":6,"name":"vr1-dc0-replication","vlans":[{"id":5193,"vid":0}]}]' ;;
esac
}
emit_spaces() {
case "$S" in
empty) echo '[{"id":-1,"name":"undefined"}]' ;;
*) echo '[{"id":1,"name":"provider-public"},{"id":2,"name":"metal-admin"},{"id":3,"name":"metal-internal"},
{"id":4,"name":"data-tenant"},{"id":5,"name":"storage"},{"id":6,"name":"replication"}]' ;;
esac
}
emit_subnets() {
case "$S" in
empty)
echo '[{"id":1,"cidr":"10.12.8.0/22","gateway_ip":null,"vlan":{"id":5001,"fabric":"fabric-0","space":"undefined"}},
{"id":2,"cidr":"fd50:840e:74e2:220::/64","gateway_ip":null,"vlan":{"id":5001,"fabric":"fabric-0","space":"undefined"}},
{"id":3,"cidr":"10.12.4.0/22","gateway_ip":"10.12.4.1","vlan":{"id":5002,"fabric":"fabric-1","space":"undefined"}}]' ;;
wrongfab)
echo '[{"id":1,"cidr":"10.12.8.0/22","gateway_ip":null,"vlan":{"id":5001,"fabric":"fabric-0","space":"metal-admin"}},
{"id":3,"cidr":"10.12.4.0/22","gateway_ip":"10.12.4.1","vlan":{"id":5002,"fabric":"fabric-1","space":"provider-public"}},
{"id":4,"cidr":"10.12.12.0/22","gateway_ip":null,"vlan":{"id":5190,"fabric":"vr1-dc0-metal-internal","space":"metal-internal"}},
{"id":5,"cidr":"10.12.16.0/22","gateway_ip":null,"vlan":{"id":5191,"fabric":"vr1-dc0-data-tenant","space":"data-tenant"}},
{"id":6,"cidr":"10.12.32.0/22","gateway_ip":null,"vlan":{"id":5192,"fabric":"vr1-dc0-storage","space":"storage"}},
{"id":7,"cidr":"10.12.36.0/22","gateway_ip":null,"vlan":{"id":5193,"fabric":"vr1-dc0-replication","space":"replication"}}]' ;;
nospace)
echo '[{"id":1,"cidr":"10.12.8.0/22","gateway_ip":null,"vlan":{"id":5001,"fabric":"fabric-0","space":"metal-admin"}},
{"id":3,"cidr":"10.12.4.0/22","gateway_ip":"10.12.4.1","vlan":{"id":5189,"fabric":"vr1-dc0-provider-public","space":"undefined"}},
{"id":4,"cidr":"10.12.12.0/22","gateway_ip":null,"vlan":{"id":5190,"fabric":"vr1-dc0-metal-internal","space":"metal-internal"}},
{"id":5,"cidr":"10.12.16.0/22","gateway_ip":null,"vlan":{"id":5191,"fabric":"vr1-dc0-data-tenant","space":"data-tenant"}},
{"id":6,"cidr":"10.12.32.0/22","gateway_ip":null,"vlan":{"id":5192,"fabric":"vr1-dc0-storage","space":"storage"}},
{"id":7,"cidr":"10.12.36.0/22","gateway_ip":null,"vlan":{"id":5193,"fabric":"vr1-dc0-replication","space":"replication"}}]' ;;
*)
echo '[{"id":1,"cidr":"10.12.8.0/22","gateway_ip":null,"vlan":{"id":5001,"fabric":"fabric-0","space":"metal-admin"}},
{"id":3,"cidr":"10.12.4.0/22","gateway_ip":"10.12.4.1","vlan":{"id":5189,"fabric":"vr1-dc0-provider-public","space":"provider-public"}},
{"id":4,"cidr":"10.12.12.0/22","gateway_ip":null,"vlan":{"id":5190,"fabric":"vr1-dc0-metal-internal","space":"metal-internal"}},
{"id":5,"cidr":"10.12.16.0/22","gateway_ip":null,"vlan":{"id":5191,"fabric":"vr1-dc0-data-tenant","space":"data-tenant"}},
{"id":6,"cidr":"10.12.32.0/22","gateway_ip":null,"vlan":{"id":5192,"fabric":"vr1-dc0-storage","space":"storage"}},
{"id":7,"cidr":"10.12.36.0/22","gateway_ip":null,"vlan":{"id":5193,"fabric":"vr1-dc0-replication","space":"replication"}}]' ;;
esac
}
case "$sub" in
rack-controllers) case "${STUB_RACK:-hot-kid}" in
none) echo '[]' ;;
*) # interface_set drives the stranded-link assertion.
# STUB_STRAND=1 puts enp2s0 on vlan 5002 while its
# subnet 10.12.4.0/22 lives on vlan 5189 -- exactly the
# 2026-07-30 defect.
if [ "${STUB_STRAND:-0}" = "1" ]; then IV=5002; else IV=5189; fi
printf '[{"hostname":"%s","interface_set":[{"name":"enp1s0","vlan":{"id":5001},"links":[{"mode":"static","ip_address":"10.12.8.6","subnet":{"cidr":"10.12.8.0/22"}}]},{"name":"enp2s0","vlan":{"id":%s},"links":[{"mode":"static","ip_address":"10.12.4.6","subnet":{"cidr":"10.12.4.0/22"}}]}]}]\n' "${STUB_RACK:-hot-kid}" "$IV" ;;
esac ;;
machines) echo '[]' ;;
fabrics) case "${1:-read}" in read) emit_fabrics ;; create) echo '{"id":99}' ;; esac ;;
spaces) case "${1:-read}" in read) emit_spaces ;; create) echo '{"id":99}' ;; esac ;;
subnets) case "${1:-read}" in read) emit_subnets ;; create) echo '{"id":99}' ;; esac ;;
subnet) case "${1:-}" in
read) case "$S" in
empty) echo '{"dns_servers":[],"allow_dns":true}' ;;
*) echo '{"dns_servers":["10.12.8.3"],"allow_dns":false}' ;;
esac ;;
reserved-ip-ranges) case "$S" in
empty) echo '[]' ;;
*) echo '[{"purpose":["dynamic"]}]' ;;
esac ;;
update) echo '{}' ;;
esac ;;
vlans) echo '[{"id":5189,"vid":0},{"id":5190,"vid":0},{"id":5191,"vid":0},{"id":5192,"vid":0},{"id":5193,"vid":0},{"id":5001,"vid":0},{"id":5002,"vid":0}]' ;;
vlan) echo '{}' ;;
tags) case "${1:-read}" in
read) case "$S" in
empty) echo '[{"name":"virtual"}]' ;;
*) echo '[{"name":"virtual"},{"name":"openstack-vr1-dc0"}]' ;;
esac ;;
create) echo '{}' ;;
esac ;;
*) echo '[]' ;;
esac
STUB
chmod +x "$TMP/bin/maas"
export PATH="$TMP/bin:$PATH"
run(){ OUT="$(bash "$SCRIPT" "$@" 2>&1)"; RC=$?; }
# --- argument validation ---------------------------------------------------
run; chk "no args -> 2" "$RC" 2
run check; chk "action only -> 2" "$RC" 2
run check vr1-dc0; chk "no --profile -> 2" "$RC" 2
case "$OUT" in *"--profile is REQUIRED"*) ok "missing profile names the OFFICE1 hazard" ;; *) no "missing profile names the hazard (got '$OUT')" ;; esac
case "$OUT" in *OFFICE1*) ok "the refusal explains WHY there is no default" ;; *) no "the refusal explains why (got '$OUT')" ;; esac
run check vr1-dc0 --profile vr1-dc0-region; chk "no --expect-rack -> 2" "$RC" 2
run bogus vr1-dc0 --profile p --expect-rack r; chk "unknown action -> 2" "$RC" 2
run check vr1-dc0 --profile p --expect-rack r --wat; chk "unknown option -> 2" "$RC" 2
# --- the REGION GATE -------------------------------------------------------
OUT="$(STUB_STATE=complete STUB_RACK=voffice1 bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "wrong region REFUSES -> 2" "$RC" 2
case "$OUT" in *"nothing was read or written"*) ok "wrong-region refusal states nothing happened" ;; *) no "wrong-region refusal states nothing happened (got '$OUT')" ;; esac
OUT="$(STUB_STATE=complete STUB_RACK=none bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "unidentifiable region (no racks) REFUSES -> 2" "$RC" 2
# and apply must be gated the same way -- a gate on check only would be useless
OUT="$(STUB_STATE=empty STUB_RACK=voffice1 bash "$SCRIPT" apply vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid --commit 2>&1)"; RC=$?
chk "APPLY --commit against the wrong region REFUSES -> 2" "$RC" 2
case "$OUT" in *APPLY*) no "wrong-region apply printed APPLY lines before refusing" ;; *) ok "wrong-region apply wrote nothing" ;; esac
# --- check must FAIL on every partial state --------------------------------
OUT="$(STUB_STATE=empty bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "freshly-inited region FAILS check -> 1" "$RC" 1
case "$OUT" in *"[FAIL] fabric 'vr1-dc0-provider-public' exists"*) ok "empty: missing named fabric FAILS" ;; *) no "empty: missing named fabric FAILS" ;; esac
case "$OUT" in *"[FAIL] space 'storage' exists"*) ok "empty: missing space FAILS" ;; *) no "empty: missing space FAILS" ;; esac
case "$OUT" in *"[FAIL] subnet 10.12.32.0/22 (storage) exists"*) ok "empty: missing plane subnet FAILS" ;; *) no "empty: missing plane subnet FAILS" ;; esac
case "$OUT" in *"[FAIL] tag 'openstack-vr1-dc0' exists"*) ok "empty: missing site tag FAILS" ;; *) no "empty: missing site tag FAILS" ;; esac
case "$OUT" in *"[FAIL] metal-admin carries node-facing dns_servers"*) ok "empty: missing D-131 dns_servers FAILS" ;; *) no "empty: missing dns_servers FAILS" ;; esac
OUT="$(STUB_STATE=wrongfab bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "subnet on the WRONG fabric FAILS check -> 1" "$RC" 1
case "$OUT" in *"[FAIL] subnet 10.12.4.0/22 is on fabric 'vr1-dc0-provider-public'"*) ok "wrongfab: names the fabric mismatch" ;; *) no "wrongfab: names the fabric mismatch (got '$OUT')" ;; esac
OUT="$(STUB_STATE=nospace bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "VLAN not bound to its space FAILS check -> 1" "$RC" 1
case "$OUT" in *"[FAIL] subnet 10.12.4.0/22 VLAN is bound to space 'provider-public'"*) ok "nospace: names the unbound space" ;; *) no "nospace: names the unbound space (got '$OUT')" ;; esac
# --- the complete state passes ---------------------------------------------
OUT="$(STUB_STATE=complete bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "complete region PASSES check -> 0" "$RC" 0
case "$OUT" in *"0 failed"*) ok "complete region reports 0 failed" ;; *) no "complete region reports 0 failed (got '$OUT')" ;; esac
# metal-admin must NOT be required to sit on a named fabric -- that is deliberate
case "$OUT" in *"metal-admin 10.12.8.0/22 is on MAAS's own fabric 'fabric-0' (by design)"*) ok "metal-admin keeps MAAS's own fabric by design" ;; *) no "metal-admin keeps MAAS's own fabric (got '$OUT')" ;; esac
# --- dry run writes nothing and says so ------------------------------------
OUT="$(STUB_STATE=empty bash "$SCRIPT" apply vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
chk "apply without --commit exits 0" "$RC" 0
case "$OUT" in *"DRY RUN complete"*) ok "dry run says it wrote nothing" ;; *) no "dry run says it wrote nothing (got '$OUT')" ;; esac
# RE-POINTED 2026-07-30: this used to assert that provider-public's fabric was
# CREATED. That pinned the DEFECTIVE behaviour -- creating a fabric and moving
# the existing subnet onto it is what stranded the region VM's enp2s0. The
# invariant is now: a plane whose subnet already sits alone on an auto fabric is
# RENAMED (asserted below), and only a plane with NO existing subnet is created.
# Replaced rather than deleted, per the standing rule that remediating a finding
# must not be done by removing the assertion that catches it.
case "$OUT" in *"DRY fabrics create name=vr1-dc0-metal-internal"*) ok "dry run plans a create for a plane with no existing subnet" ;; *) no "dry run plans a create for a plane with no existing subnet (got '$OUT')" ;; esac
case "$OUT" in *"DRY spaces create name=metal-admin"*) ok "dry run plans the spaces" ;; *) no "dry run plans the spaces" ;; esac
case "$OUT" in *APPLY*) no "dry run emitted APPLY lines" ;; *) ok "dry run emitted no APPLY lines" ;; esac
# a subnet on the wrong fabric must be planned as a MOVE, never a duplicate create
OUT="$(STUB_STATE=wrongfab bash "$SCRIPT" apply vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
case "$OUT" in *"MOVE 10.12.4.0/22 off 'fabric-1' onto 'vr1-dc0-provider-public'"*) ok "wrongfab is planned as a MOVE" ;; *) no "wrongfab is planned as a MOVE (got '$OUT')" ;; esac
case "$OUT" in *"subnets create cidr=10.12.4.0/22"*) no "wrongfab wrongly planned a duplicate subnet create" ;; *) ok "wrongfab did not plan a duplicate create" ;; esac
# --- fabric naming is DERIVED from the site token, never typed -------------
OUT="$(STUB_STATE=empty bash "$SCRIPT" apply vr1-dc1 --profile vr1-dc1-region --expect-rack hot-kid 2>&1)"; RC=$?
case "$OUT" in *"vr1-dc1-provider-public"*) ok "dc1 derives vr1-dc1-* fabric names" ;; *) no "dc1 derives vr1-dc1-* fabric names (got '$OUT')" ;; esac
case "$OUT" in *"vr1-dc0-"*) no "dc1 invocation leaked a dc0 fabric name" ;; *) ok "dc1 invocation carries no dc0 fabric name" ;; esac
# --- DRY-RUN OUTPUT MUST NOT PRINT EMPTY IDS -------------------------------
# On a from-scratch build the target fabric does not exist yet when the MOVE and
# space-bind lines are planned, so their ids cannot resolve. They must print an
# explicit placeholder: an empty `vlan=` looks like a defect and would hide one.
OUT="$(STUB_STATE=empty bash "$SCRIPT" apply vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"
case "$OUT" in *"vlan="[[:space:]]*|*"vlan= "*) no "dry run printed an EMPTY vlan= id" ;; *) ok "dry run printed no empty vlan= id" ;; esac
case "$OUT" in *"fabric ''"*) no "dry run printed an EMPTY fabric name" ;; *) ok "dry run printed no empty fabric name" ;; esac
case "$OUT" in *"<vlan-of-vr1-dc0-provider-public>"*) ok "MOVE line uses an explicit vlan placeholder" ;; *) no "MOVE line uses an explicit vlan placeholder (got '$OUT')" ;; esac
case "$OUT" in *"<fabric-for-storage>"*) ok "space-bind line uses an explicit fabric placeholder" ;; *) no "space-bind line uses an explicit fabric placeholder" ;; esac
# --- STRANDED INTERFACE LINKS ----------------------------------------------
# The 2026-07-30 defect: a subnet MOVE leaves interface links behind on the old
# VLAN. Every other assertion passed 39/39 while the model was inconsistent, and
# live connectivity was unaffected, so only an interface read catches it.
OUT="$(STUB_STATE=complete bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
case "$OUT" in *"[ok] no stranded interface links"*) ok "consistent links pass the stranded check" ;; *) no "consistent links pass the stranded check (got '$OUT')" ;; esac
chk "consistent region still exits 0" "$RC" 0
OUT="$(STUB_STATE=complete STUB_STRAND=1 bash "$SCRIPT" check vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"; RC=$?
case "$OUT" in *"[FAIL] no stranded interface links"*) ok "a STRANDED link FAILS the check" ;; *) no "a stranded link FAILS the check (got '$OUT')" ;; esac
case "$OUT" in *"enp2s0"*) ok "the stranded failure names the interface" ;; *) no "the stranded failure names the interface" ;; esac
chk "stranded region exits 1" "$RC" 1
# --- RENAME IS PREFERRED OVER MOVE -----------------------------------------
# Renaming the auto fabric that already carries the subnet moves nothing and
# leaves interface links intact; creating + moving is what stranded enp2s0.
OUT="$(STUB_STATE=empty bash "$SCRIPT" apply vr1-dc0 --profile vr1-dc0-region --expect-rack hot-kid 2>&1)"
case "$OUT" in *"RENAME 'fabric-1'"*) ok "an auto fabric carrying only this plane is RENAMED" ;; *) no "auto fabric is RENAMED (got '$OUT')" ;; esac
case "$OUT" in *"fabrics create name=vr1-dc0-provider-public"*) no "provider-public was CREATED instead of renamed (the stranding path)" ;; *) ok "provider-public was not created-and-moved" ;; esac
case "$OUT" in *"fabrics create name=vr1-dc0-storage"*) ok "a plane with no existing subnet is still CREATED" ;; *) no "a plane with no existing subnet is still created" ;; esac
echo
echo "dc-region-topology: $P passed, $F failed"
[ "$F" -eq 0 ] || exit 1