diff --git a/docs/phase-00-maas-standup-notes.md b/docs/phase-00-maas-standup-notes.md index 102b80e..7e89dbb 100644 --- a/docs/phase-00-maas-standup-notes.md +++ b/docs/phase-00-maas-standup-notes.md @@ -23,11 +23,11 @@ planes are created first so their tagged siblings can ride the same fabric (provider-public->provider-vip, metal-admin->metal-internal). -## Scope boundary -- THIS script owns topology + the three API-VIP reserve bands (the bundle deploy - depends on those reserves existing). -- `phase-00-maas-carve.sh` keeps the FIP pool, mgmt reserves, and stale-range - cleanup. (Its D-058 CIDR update is the next pass.) +## Single MAAS-address authority (D-058 consolidation) +- THIS script owns topology AND every reserved range: the API-VIP bands, the + Neutron FIP pool (10.12.5.0-.7.254 on provider-public), and the mgmt reserves. +- `phase-00-maas-carve.sh` is RETIRED -- its reserves are folded in here, and its + gated stale-range delete is subsumed by the teardown + re-CIDR step. - It never deletes anything. ## Relationship to provider-vip-standup.sh diff --git a/scripts/phase-00-maas-standup.sh b/scripts/phase-00-maas-standup.sh index f88612b..01db208 100644 --- a/scripts/phase-00-maas-standup.sh +++ b/scripts/phase-00-maas-standup.sh @@ -19,10 +19,11 @@ # human teardown step. This script will refuse to create a target subnet whose # CIDR is occupied by the wrong plane. # -# Scope boundary vs phase-00-maas-carve.sh: THIS script owns topology (fabric/ -# VLAN/subnet/space/gateway/managed/dns) + the per-plane API-VIP reserve bands -# (.2-.100 on the three VIP-bearing planes), which the bundle deploy depends on. -# The FIP pool, mgmt reserves, and stale-range cleanup stay in phase-00-maas-carve. +# SINGLE MAAS-address authority (D-058 consolidation): owns topology (fabric/VLAN/ +# subnet/space/gateway/managed/dns) AND every reserved range -- API-VIP bands, the +# Neutron FIP pool, and mgmt reserves. phase-00-maas-carve.sh is RETIRED: its FIP/ +# VIP/mgmt reserves are folded in here, and its gated stale-range delete is subsumed +# by the teardown + re-CIDR step. # # Order matters (MAAS semantics + fresh-fabric bootstrap): untagged base planes # first (each owns a fabric), then their tagged siblings ride that fabric, so a @@ -81,17 +82,18 @@ vlanspace() { case "$1" in "<"*) return;; esac; maas_json vlans read "$1" | jq -r --arg v "$2" '.[]|select((.vid|tostring)==$v)|(.space // "")' | head -1; } vlan0obj() { vlanobj "$1" 0; } # the untagged (vid 0) default VLAN of a fabric -# --- target plane table (D-058): name|cidr|kind|vid|parent_cidr|gw|viplo|viphi|dnssrc +# --- target plane table (D-058): name|cidr|kind|vid|parent_cidr|gw|dnssrc|reserves +# reserves = ";"-separated "lo:hi:label" entries (or "-"); label has no : ; | # kind=untagged owns a fabric; kind=tagged rides parent_cidr's fabric on . # "-" = none. dnssrc = a CIDR whose dns_servers to mirror, or "-". PLANES="$(cat </dev/null 2>&1; then - note "reserved range starting $viplo exists -- SKIP" - else - rsid="$(sub_id "$cidr")"; [ -n "$rsid" ] || rsid="" - emit "create reserved API-VIP band $viplo-$viphi on subnet $rsid" \ - ipranges create type=reserved subnet="$rsid" start_ip="$viplo" end_ip="$viphi" \ - comment="$name API HA VIP band (D-058)" - fi + # ---- reserved ranges (API-VIP bands, FIP pool, mgmt) -- D-058 single authority ---- + if [ -n "$reserves" ]; then + IPR="$(maas_json ipranges read)" + IFS=';' read -ra RES <<< "$reserves" + for r in "${RES[@]}"; do + rlo="${r%%:*}"; rrest="${r#*:}"; rhi="${rrest%%:*}"; rlabel="${rrest#*:}" + if printf '%s' "$IPR" | jq -e --arg lo "$rlo" '.[]|select(.start_ip==$lo)' >/dev/null 2>&1; then + note "reserved range starting $rlo exists -- SKIP" + else + rsid="$(sub_id "$cidr")"; [ -n "$rsid" ] || rsid="" + emit "create reserved range $rlo-$rhi ($rlabel) on subnet $rsid" \ + ipranges create type=reserved subnet="$rsid" start_ip="$rlo" end_ip="$rhi" comment="$rlabel" + fi + done fi done <<< "$PLANES" diff --git a/tests/make_fixtures.py b/tests/make_fixtures.py new file mode 100644 index 0000000..d63f2de --- /dev/null +++ b/tests/make_fixtures.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# tests/phase-00-maas-standup/make_fixtures.py +# Emits fix/_{subnets,spaces,vlans,ipranges,fabrics}.json for the +# phase-00-maas-standup.sh behavior harness. ASCII + LF. +import json, os + +HERE = os.path.dirname(os.path.abspath(__file__)) +FIX = os.path.join(HERE, "fix") +os.makedirs(FIX, exist_ok=True) + + +def sub(cidr, sid, space, vid, fab, mtu=1500, gw=None, managed=True, dns=None): + return {"cidr": cidr, "id": sid, "space": space, + "vlan": {"vid": vid, "fabric_id": fab, "mtu": mtu}, + "gateway_ip": gw, "managed": managed, "dns_servers": dns or []} + + +def vlan(vid, vid_id, fab, space, mtu=1500): + return {"vid": vid, "id": vid_id, "fabric_id": fab, "space": space, "mtu": mtu} + + +def dump(scn, subnets, spaces, vlans, ipranges, fabrics): + for name, obj in (("subnets", subnets), ("spaces", spaces), ("vlans", vlans), + ("ipranges", ipranges), ("fabrics", fabrics)): + with open(os.path.join(FIX, f"{scn}_{name}.json"), "w") as f: + json.dump(obj, f, indent=2) + f.write("\n") + + +def spaces_list(names): + return [{"name": n, "id": i + 1} for i, n in enumerate(names)] + + +def fabrics_list(pairs): # [(name,id)] + return [{"name": n, "id": i} for n, i in pairs] + + +# ---- FRESH: nothing exists -> full create plan ---- +dump("fresh", [], [], [], [], []) + +# ---- DONE: D-058 fully present + correct -> all SKIP, zero WOULD ---- +fabs = fabrics_list([("provider", 1), ("metal", 2), ("data", 3), ("storage", 4), ("replication", 5)]) +vl = [ + vlan(0, 10, 1, "provider-public"), vlan(104, 11, 1, "provider-vip"), + vlan(0, 20, 2, "metal-admin"), vlan(103, 21, 2, "metal-internal", mtu=9000), + vlan(0, 30, 3, "data-tenant"), vlan(0, 40, 4, "storage"), vlan(0, 50, 5, "replication"), +] +subs = [ + sub("10.12.4.0/22", 1, "provider-public", 0, 1, gw="10.12.4.1"), + sub("10.12.8.0/22", 2, "provider-vip", 104, 1, gw="10.12.8.1"), # dns mirrors metal-internal (which is unset) -> left unset + sub("10.12.12.0/22", 3, "metal-admin", 0, 2, gw="10.12.12.1"), + sub("10.12.16.0/22", 4, "metal-internal", 103, 2, mtu=9000), + sub("10.12.20.0/22", 5, "data-tenant", 0, 3), + sub("10.12.32.0/22", 6, "storage", 0, 4), + sub("10.12.36.0/22", 7, "replication", 0, 5), +] +spc = spaces_list(["provider-public", "provider-vip", "metal-admin", "metal-internal", + "data-tenant", "storage", "replication"]) +ipr = [{"type": "reserved", "start_ip": lo, "end_ip": hi, "subnet": {"id": sid}} + for lo, hi, sid in [("10.12.5.0", "10.12.7.254", 1), # FIP pool (provider-public) + ("10.12.4.101", "10.12.4.110", 1), # provider-public mgmt + ("10.12.8.2", "10.12.8.100", 2), # provider-vip API VIP + ("10.12.12.2", "10.12.12.100", 3), # metal-admin API VIP + ("10.12.12.101", "10.12.12.110", 3), # metal-admin mgmt + ("10.12.16.2", "10.12.16.100", 4)]] # metal-internal API VIP +dump("done", subs, spc, vl, ipr, fabs) + +# ---- D-052 CURRENT (old live scheme): the three migrating planes drift ---- +fabs_c = fabrics_list([("1_provider", 1), ("2_metal", 2), ("4_data", 3), + ("8_storage", 4), ("9_replication", 5)]) +vl_c = [ + vlan(0, 10, 1, "provider-public"), + vlan(0, 20, 2, "metal-admin"), vlan(103, 21, 2, "metal-internal", mtu=9000), + vlan(0, 30, 3, "data-tenant"), vlan(0, 40, 4, "storage"), vlan(0, 50, 5, "replication"), +] +subs_c = [ + sub("10.12.4.0/22", 1, "provider-public", 0, 1, gw="10.12.4.1"), # correct under D-058 -> SKIP + sub("10.12.8.0/22", 2, "metal-admin", 0, 2, gw="10.12.8.1"), # D-058 wants provider-vip -> DRIFT + sub("10.12.12.0/22", 3, "metal-internal", 103, 2, mtu=9000), # D-058 wants metal-admin -> DRIFT + sub("10.12.16.0/22", 4, "data-tenant", 0, 3), # D-058 wants metal-internal -> DRIFT + sub("10.12.32.0/22", 6, "storage", 0, 4), # correct -> SKIP + sub("10.12.36.0/22", 7, "replication", 0, 5), # correct -> SKIP +] +spc_c = spaces_list(["provider-public", "metal-admin", "metal-internal", + "data-tenant", "storage", "replication"]) +dump("d052", subs_c, spc_c, vl_c, [], fabs_c) + +# ---- WRONG-VID: provider-vip subnet present at .8 but on VID 99 ---- +vl_w = [v for v in vl if v["vid"] != 104] + [vlan(99, 11, 1, "provider-vip")] +subs_w = [s for s in subs if s["cidr"] != "10.12.8.0/22"] + \ + [sub("10.12.8.0/22", 2, "provider-vip", 99, 1, gw="10.12.8.1")] +dump("wrongvid", subs_w, spc, vl_w, ipr, fabs) + +print("fixtures written to", FIX) diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100644 index 0000000..60ec8d9 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Behavior regression for phase-00-maas-standup.sh (D-058). Fake `maas` + real jq. +# Drives DRY-RUN and asserts WOULD/SKIP/DRIFT/refuse behaviour across scenarios. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$(cd "$HERE/../../scripts" && pwd)/phase-00-maas-standup.sh" +BIN="$HERE/fakebin"; FIX="$HERE/fix" +chmod +x "$BIN"/* 2>/dev/null || true # GitHub Desktop lands files mode 100644 +command -v jq >/dev/null || { echo "FAIL: jq required"; exit 1; } +python3 "$HERE/make_fixtures.py" >/dev/null +rc_all=0; OUT="$(mktemp)" + +run() { #