Newer
Older
openstack-caracal-dc-dc / tests / dc-region-topology / run-tests.sh
#!/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 '[]' ;;
                      *) printf '[{"hostname":"%s"}]\n' "${STUB_RACK:-hot-kid}" ;;
                    esac ;;
  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
case "$OUT" in *"DRY    fabrics create name=vr1-dc0-provider-public"*) ok "dry run plans the named fabrics" ;; *) no "dry run plans the named fabrics (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

echo
echo "dc-region-topology: $P passed, $F failed"
[ "$F" -eq 0 ] || exit 1