Newer
Older
openstack-caracal-dc-dc / tests / dc-plane-ipam / run-tests.sh
#!/usr/bin/env bash
# tests/dc-plane-ipam/run-tests.sh -- offline harness for scripts/dc-plane-ipam.sh.
#
# Stubs the `maas` client with a fakebin returning controlled JSON, and points the
# tool at fixture apex records. Mutates nothing outside $TMP; never touches live MAAS.
#
# THE POINT OF T6: everything this gate asserts is currently ABSENT live, so every
# real run FAILS. A gate only ever observed failing is as untrustworthy as one only
# ever observed passing -- T6 proves it CAN go green on a fully-provisioned fixture,
# so a future green is meaningful rather than merely unobserved.
# Exit: 0 all pass | 1 any case failed.  ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0

ok()  { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; shift; [ $# -gt 0 ] && printf '%s\n' "$*" | sed 's/^/        /'; }

# ---- fixture apex record: the six dc0 plane /64s + the provider VIP /64 --------
mkapex() { # mkapex <file> [omit-role]
  python3 - "$1" "${2:-}" <<'PY'
import json, sys
out, omit = [], sys.argv[2]
planes = [("metal-admin","fd50:840e:74e2:220::/64"),("metal-internal","fd50:840e:74e2:221::/64"),
          ("data-tenant","fd50:840e:74e2:230::/64"),("storage","fd50:840e:74e2:240::/64"),
          ("replication","fd50:840e:74e2:250::/64"),("provider-public","2602:f3e2:f02:10::/64")]
for role, pre in planes:
    if role == omit: continue
    out.append({"prefix":pre,"scope":{"name":"vr1-dc0"},"role":{"slug":role},
                "description":"VR1 DC0 %s (ULA /64 active)" % role})
out.append({"prefix":"2602:f3e2:f02:11::/64","scope":{"name":"vr1-dc0"},
            "role":{"slug":"provider-public"},"description":"VR1 DC0 provider-public (GUA VIP /64)"})
out.append({"prefix":"fd50:840e:74e2:220::/60","scope":{"name":"vr1-dc0"},
            "role":{"slug":"metal-admin"},"description":"parent /60 -- must be IGNORED"})
json.dump({"ipam/prefixes": out}, open(sys.argv[1], "w"))
PY
}

# ---- fakebin maas -------------------------------------------------------------
# <dir> <v6:full|none> <bands:full|none|wrongtype> [subnets_rc] [ranges_rc]
mkmaas() {
  local d="$1" v6="$2" bands="$3" src="${4:-0}" rrc="${5:-0}"
  mkdir -p "$d/fakebin"
  cat > "$d/fakebin/maas" <<FB
#!/usr/bin/env bash
if [ "\$2" = "subnets" ]; then
  [ "$src" -ne 0 ] && exit $src
  python3 - "$v6" <<'PY'
import json,sys
v6=sys.argv[1]
s=[{"cidr":c,"id":i,"vlan":{"id":5000+i,"fabric":"f-%d"%i}} for i,c in enumerate(
   ["10.12.4.0/22","10.12.8.0/22","10.12.12.0/22","10.12.16.0/22","10.12.32.0/22","10.12.36.0/22"])]
if v6=="full":
    for i,c in enumerate(["fd50:840e:74e2:220::/64","fd50:840e:74e2:221::/64","fd50:840e:74e2:230::/64",
                          "fd50:840e:74e2:240::/64","fd50:840e:74e2:250::/64","2602:f3e2:f02:10::/64"]):
        s.append({"cidr":c,"id":100+i,"vlan":{"id":5000+i,"fabric":"f-%d"%i}})
print(json.dumps(s))
PY
  exit 0
fi
if [ "\$2" = "subnet" ] && [ "\$3" = "reserved-ip-ranges" ]; then
  # MAAS's MEASURED default on every IPv6 subnet (2026-07-27, live): the whole
  # ::1-::ffff:ffff block is already reserved, plus :: per RFC 4291 s2.6.1.
  echo '[{"start":"fd50:840e:74e2:220::","end":"fd50:840e:74e2:220::","purpose":["rfc-4291-2.6.1"]},{"start":"fd50:840e:74e2:220::1","end":"fd50:840e:74e2:220::ffff:ffff","purpose":["reserved"]}]'
  exit 0
fi
if [ "\$2" = "ipranges" ]; then
  [ "$rrc" -ne 0 ] && exit $rrc
  python3 - "$bands" <<'PY'
import json,sys
b=sys.argv[1]; r=[]
if b in ("full","wrongtype"):
    t = "dynamic" if b=="wrongtype" else "reserved"
    for third in (4,8,12,16,32,36):
        r.append({"type":t,"start_ip":"10.12.%d.4"%third,"end_ip":"10.12.%d.49"%third})
        r.append({"type":t,"start_ip":"10.12.%d.50"%third,"end_ip":"10.12.%d.99"%third})
print(json.dumps(r))
PY
  exit 0
fi
exit 1
FB
  chmod +x "$d/fakebin/maas"
}

run() { # run <want_rc> <regex> <label> <dir> [apex]
  local want="$1" re="$2" label="$3" d="$4"
  local apex="${5:-$d/apex.json}"   # split: bash expands ALL words of a `local`
                                     # before assigning any, so $d is unset above
  local out rc
  out="$(cd "$REPO" && PATH="$d/fakebin:$PATH" APEX_RECORD="$apex" \
         bash scripts/dc-plane-ipam.sh check vr1-dc0 2>&1)"; rc=$?
  if [ "$rc" = "$want" ] && grep -qE "$re" <<<"$out"; then ok "$label"
  else bad "$label (rc=$rc want=$want)" "$(printf '%s' "$out" | tail -4)"; fi
}

echo "== dc-plane-ipam =="

# ---- argument / selector guards ----
for a in "check dc0:2:RETIRED" "check dc1:2:RETIRED" "bogus vr1-dc0:2:usage" "check:2:usage"; do
  args="${a%%:*}"; rest="${a#*:}"; want="${rest%%:*}"; re="${rest##*:}"
  out="$(cd "$REPO" && bash scripts/dc-plane-ipam.sh $args 2>&1)"; rc=$?
  if [ "$rc" = "$want" ] && grep -qiE "$re" <<<"$out"; then ok "T-guard '$args' -> rc$want ($re)"
  else bad "T-guard '$args' (rc=$rc want=$want)" "$out"; fi
done

# ---- T1 the fully-absent baseline: the live state today ----
D="$TMP/absent"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" none none
run 1 'v6 metal-admin .* ABSENT' "T1 no v6 and no bands -> FAIL (the measured live baseline)" "$D"

# ---- T2/T3 "could not look" must never read as clean ----
D="$TMP/subrc"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full full 4 0
run 2 'REFUSE' "T2 'maas subnets read' failing REFUSES (exit 2), never reports clean" "$D"
D="$TMP/rngrc"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full full 0 4
run 2 'REFUSE' "T3 'maas ipranges read' failing REFUSES (exit 2)" "$D"

# ---- T4 an unreadable apex REFUSES rather than asserting over zero planes ----
D="$TMP/noapex"; mkdir -p "$D"; mkmaas "$D" full full
run 2 'REFUSE' "T4 unreadable apex record REFUSES -- never 'no v6 planes, all clear'" "$D" "$D/nope.json"

# ---- T5 bands present but the WRONG TYPE: unrecognised state must not pass ----
D="$TMP/wrongtype"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full wrongtype
run 1 "type='dynamic'" "T5 band exists as 'dynamic' not 'reserved' -> FAIL, not a pass" "$D"

# ---- T6 THE CRITICAL ONE: a fully-provisioned fixture must PASS ----
D="$TMP/full"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full full
run 0 'PASS: dc-plane-ipam' "T6 fully-provisioned fixture PASSES -- the gate can go green" "$D"

# ---- T7 one missing v6 plane is caught (not masked by the other five) ----
D="$TMP/one"; mkdir -p "$D"; mkapex "$D/apex.json" storage; mkmaas "$D" full full
run 0 'PASS: dc-plane-ipam' "T7a apex omitting a plane does not invent a failure" "$D"
D="$TMP/one2"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" none full
run 1 'v6 storage .* ABSENT' "T7b a v6 plane absent from MAAS is named individually" "$D"

# ---- T8 the parent /60 must be IGNORED, not treated as a plane ----
D="$TMP/p60"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full full
out="$(cd "$REPO" && PATH="$D/fakebin:$PATH" APEX_RECORD="$D/apex.json" \
       bash scripts/dc-plane-ipam.sh check vr1-dc0 2>&1)"
if ! grep -q '220::/60' <<<"$out"; then ok "T8 parent /60 blocks are ignored, not asserted as planes"
else bad "T8 a /60 parent block was treated as a plane" "$out"; fi

# ---- T9 the provider VIP /64 is reported but NOT asserted ----
if grep -q 'provider VIP block' <<<"$out" && ! grep -qE '\[FAIL\].*f02:11::/64' <<<"$out"; then
  ok "T9 provider VIP /64 is reported for visibility, deliberately not asserted"
else bad "T9 provider VIP /64 handling wrong" "$out"; fi

# ============ carve-v6 / reserve (dry by default) ============================
runact() { # runact <action> <want_rc> <regex> <label> <dir> [extra-args...]
  local act="$1" want="$2" re="$3" label="$4" d="$5"; shift 5
  local out rc
  out="$(cd "$REPO" && PATH="$d/fakebin:$PATH" APEX_RECORD="$d/apex.json" \
         bash scripts/dc-plane-ipam.sh "$act" vr1-dc0 "$@" 2>&1)"; rc=$?
  if [ "$rc" = "$want" ] && grep -qE "$re" <<<"$out"; then ok "$label"
  else bad "$label (rc=$rc want=$want)" "$(printf '%s' "$out" | tail -5)"; fi
}

# --- carve-v6 ---
D="$TMP/carve"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" none none
runact carve-v6 0 'planned=6 applied=0' "T10 carve-v6 dry run plans 6 and writes NOTHING" "$D"
runact carve-v6 0 'DRY RUN -- nothing was written' "T11 carve-v6 says plainly that it is a dry run" "$D"
runact carve-v6 0 'provider VIP block, deliberately not a MAAS subnet' \
  "T12 carve-v6 SKIPS the provider VIP /64 rather than creating it" "$D"
D="$TMP/carve2"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full none
runact carve-v6 0 'planned=0 applied=0 skipped=7' "T13 carve-v6 is IDEMPOTENT -- all present, nothing planned" "$D"

# --- reserve ---
D="$TMP/res"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" none none
runact reserve 0 'reserve 10.12.4.50-10.12.4.99' "T14 reserve plans the D-134 VIP band" "$D"
runact reserve 0 'reserve 10.12.4.4-10.12.4.49'  "T15 reserve plans the D-134 utility band" "$D"
runact reserve 0 'not in MAAS yet -- run carve-v6 first' \
  "T16 v6 bands SKIP until carve-v6 has run (forced sequencing, not a silent pass)" "$D"
D="$TMP/res2"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" none full
runact reserve 0 'already reserved' "T17 reserve is IDEMPOTENT -- existing reserved bands are skipped" "$D"
D="$TMP/res3"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" none wrongtype
runact reserve 1 "REFUSING to rewrite an existing range" \
  "T18 a same-bounds range of the WRONG type REFUSES rather than being overwritten" "$D"

# --- T19: the v6 band mirror is TEXTUAL, not numeric ---
# REPLACED 2026-07-27, not deleted. The original asserted that the tool PLANS a
# reserved range `...::50-...::99`. Measurement then showed MAAS AUTO-RESERVES
# ::1-::ffff:ffff on every IPv6 subnet, so an explicit create is both impossible
# ("conflicts with an existing range") and unnecessary -- the tool now VERIFIES
# coverage instead of writing. Remediating the finding turned this assertion red,
# so it is re-pointed at the surviving invariant rather than removed:
# the bounds checked must be the TEXTUAL octet mirror (4-99), never the hex
# conversion (4-63) that `printf '%x'` would have produced.
D="$TMP/v6band"; mkdir -p "$D"; mkapex "$D/apex.json"; mkmaas "$D" full none
out="$(cd "$REPO" && PATH="$D/fakebin:$PATH" APEX_RECORD="$D/apex.json" \
       bash scripts/dc-plane-ipam.sh reserve vr1-dc0 2>&1)"
if grep -q "bands 4-99 already protected" <<<"$out" && ! grep -q 'bands 4-63' <<<"$out"; then
  ok "T19 v6 bands verified over the TEXTUAL octet range 4-99, not the hex 4-63"
else bad "T19 v6 band bounds wrong" "$(grep -iE 'band|protect' <<<"$out" | head -3)"; fi

# --- T20: dc1 must REFUSE to invent the FIP pool ---
# lib-net deliberately unsets FIP_POOL_* for dc1 ("UNSET so any use fails loud").
# Mirroring dc0's shape would be an inferred value -- hard rule 2.
mkapex1() { python3 - "$1" <<'PY2'
import json,sys
out=[{"prefix":"fd50:840e:74e2:320::/64","scope":{"name":"vr1-dc1"},
      "role":{"slug":"metal-admin"},"description":"VR1 DC1 metal-admin (ULA /64 active)"}]
json.dump({"ipam/prefixes":out},open(sys.argv[1],"w"))
PY2
}
# REPLACED 2026-07-27, not deleted. This asserted dc1 REFUSES the FIP pool because it
# was unruled. It was ruled that day ("Rule dc1 FIP pool 10.12.65.0-10.12.67.254"), so
# the refusal path is no longer reachable for dc1. Re-pointed at the ruled value: the
# tool must now PLAN the ruled pool, and must plan dc0's DIFFERENT pool for dc0 -- which
# together prove it reads the per-DC value rather than a shared literal. The generic
# "refuse rather than infer" behaviour is still covered, by the FIP_POOL_START guard in
# the script and by tests/dc-selector's still-unset assertions.
D="$TMP/dc1fip"; mkdir -p "$D"; mkapex1 "$D/apex.json"; mkmaas "$D" none none
out="$(cd "$REPO" && PATH="$D/fakebin:$PATH" APEX_RECORD="$D/apex.json" \
       bash scripts/dc-plane-ipam.sh reserve vr1-dc1 2>&1)"
if grep -q '10.12.65.0-10.12.67.254' <<<"$out" && ! grep -q '10.12.5.0-10.12.7.254' <<<"$out"; then
  ok "T20 dc1 plans its OWN ruled FIP pool (.65-.67), never dc0's (.5-.7)"
else bad "T20 dc1 FIP pool wrong" "$(grep -i fip <<<"$out" | head -3)"; fi

echo

# ============================================================================
# retire-v6-ula (D-139 step 6, MAAS half)
# ============================================================================
# R3 is the case that matters most: a subnet with NO allocation but WITH an
# interface link. The by-hand check used on dc0 2026-08-02 had a quoting bug
# (the subnet id was interpolated as "'2'" and never matched), so it returned
# ZERO links for every subnet and could not have failed. The tool found a real
# link the moment it ran. This case exists so that cannot recur silently.
mkmaas_retire() { # <dir> <holders:none|sticky> <links:none|one> [delete_sticks:0|1] [machines_rc]
  local d="$1" holders="$2" links="$3" sticks="${4:-0}" mrc="${5:-0}"
  mkdir -p "$d/fakebin"
  cat > "$d/fakebin/maas" <<FB
#!/usr/bin/env bash
STATE="$d/deleted"
if [ "\$2" = "subnets" ]; then
  python3 - "\$STATE" <<'PY'
import json,os,sys
gone=set()
if os.path.exists(sys.argv[1]): gone={l.strip() for l in open(sys.argv[1])}
s=[{"cidr":"10.12.4.0/22","id":1,"vlan":{"id":5001}}]
for i,c in enumerate(["fd50:840e:74e2:221::/64","fd50:840e:74e2:230::/64"]):
    if str(200+i) not in gone: s.append({"cidr":c,"id":200+i,"vlan":{"id":5001}})
print(json.dumps(s))
PY
  exit 0
fi
if [ "\$2" = "ipranges" ]; then echo '[]'; exit 0; fi
if [ "\$2" = "machines" ]; then
  [ "$mrc" -ne 0 ] && exit $mrc
  if [ "$links" = "one" ]; then
    echo '[{"hostname":"node-a","interface_set":[{"name":"enp1s0","links":[{"subnet":{"id":200}}]}]}]'
  else
    echo '[{"hostname":"node-a","interface_set":[{"name":"enp1s0","links":[]}]}]'
  fi
  exit 0
fi
if [ "\$2" = "subnet" ] && [ "\$3" = "ip-addresses" ]; then
  if [ "$holders" = "sticky" ] && [ "\$4" = "200" ]; then
    echo '[{"ip":"fd50:840e:74e2:221::5","alloc_type":1,"user":"juju-vr1-dc0"}]'
  else
    echo '[]'
  fi
  exit 0
fi
if [ "\$2" = "subnet" ] && [ "\$3" = "delete" ]; then
  [ "$sticks" -eq 1 ] || echo "\$4" >> "\$STATE"
  exit 0
fi
if [ "\$2" = "subnet" ] && [ "\$3" = "read" ]; then
  if [ -f "\$STATE" ] && grep -qx "\$4" "\$STATE"; then exit 1; fi
  echo '{"id":'"\$4"',"cidr":"fd50:840e:74e2:221::/64"}'; exit 0
fi
exit 1
FB
  chmod +x "$d/fakebin/maas"
  rm -f "$d/deleted"
}

run_retire() { # run_retire <want_rc> <regex> <label> <dir> [--commit]
  local want="$1" re="$2" label="$3" d="$4" extra="${5:-}"
  local out rc
  out="$(cd "$REPO" && PATH="$d/fakebin:$PATH" \
         bash scripts/dc-plane-ipam.sh retire-v6-ula vr1-dc0 $extra 2>&1)"; rc=$?
  if [ "$rc" = "$want" ] && grep -qE "$re" <<<"$out"; then ok "$label"
  else bad "$label (rc=$rc want=$want)" "$(printf '%s' "$out" | tail -5)"; fi
}

D2="$(mktemp -d)"
mkmaas_retire "$D2" none none
run_retire 0 'EMPTY -- would delete' "R1 empty ULA subnets are planned for deletion (dry)" "$D2"

mkmaas_retire "$D2" sticky none
run_retire 1 'is NOT empty -- REFUSING' "R2 a sticky allocation REFUSES the delete" "$D2"
mkmaas_retire "$D2" sticky none
run_retire 1 'user=juju-vr1-dc0' "R3 the refusal NAMES the holder, not just a count" "$D2"

mkmaas_retire "$D2" none one
run_retire 1 'interface links: node-a:enp1s0' "R4 an interface link with NO allocation still REFUSES (the dc0 blind spot)" "$D2"

mkmaas_retire "$D2" none none
run_retire 0 'deleted and verified gone' "R5 --commit deletes and reads back that the row is gone" "$D2" --commit

mkmaas_retire "$D2" none none 1
run_retire 1 'STILL READABLE' "R6 a delete that returns success but leaves the row REPORTS it (read-back has teeth)" "$D2" --commit

mkmaas_retire "$D2" none none 0 2
run_retire 2 'REFUSE' "R7 'machines read' failing REFUSES -- could not look is never nothing there" "$D2"
rm -rf "$D2"

echo "RESULT: PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] || exit 1