diff --git a/scripts/dc-plane-ipam.sh b/scripts/dc-plane-ipam.sh new file mode 100755 index 0000000..796de6a --- /dev/null +++ b/scripts/dc-plane-ipam.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# scripts/dc-plane-ipam.sh +# +# Site-keyed per-DC plane IPAM state for VR1. Ruled by the D-134 AMENDMENT of +# 2026-07-27 (R4): "a site-keyed reservation tool on the dc-mirror.sh / +# dc-rack-net.sh pattern (which also gives D-134 an EXECUTABLE gate instead of +# prose)". THIS FILE IS THAT GATE. Until it existed, D-134's band table lived +# only as decision prose and MAAS held ZERO reserved ranges -- a ruled decision +# with no artifact, which is the class the 2026-07-27 audit found twice. +# +# ACTIONS +# check READ-ONLY. Compare live MAAS against the ruled expected state +# and report per-plane. Exit 0 only if every assertion passes. +# +# The MUTATING actions (v6 carve, band reservation) are NOT in this file yet -- +# hard rule 3, verify before mutate: the read-only gate ships and runs first, so +# the mutation is planned against a MEASURED baseline rather than an assumed one. +# +# EXPECTED STATE IS DERIVED, NEVER HARDCODED (hard rule 3): +# v4 plane CIDRs <- scripts/lib-net.sh via lib_net_select_dc (site-keyed) +# v6 plane /64s <- the NetBox APEX record (office1-netbox is the VR1 working +# apex, DOCFIX-195). D-136 option (D), ADOPTED 2026-07-27, +# makes the apex the source these values come from; reading +# it here rather than carrying a second hand-maintained +# table is that adoption applied. +# band table <- the D-134 2026-07-23 AMENDMENT (the amendment is +# authoritative; the original table above it is SUPERSEDED). +# +# Exit: 0 all assertions pass | 1 assertion(s) failed | 2 could not evaluate. +# "Could not look" is NEVER "nothing there" -- an unreachable MAAS or an +# unreadable apex record REFUSES (exit 2) instead of reporting a clean tree. +# ASCII + LF. +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "$HERE/.." && pwd)" +ACTION="${1:-}"; SITE="${2:-}" +MAAS_PROFILE="${MAAS_PROFILE:-admin}" + +usage() { + cat >&2 < + site: vr1-dc0 | vr1-dc1 (region-qualified per D-119; bare dc0/dc1 are RETIRED) +env: MAAS_PROFILE (default: admin) + APEX_RECORD (default: newest netbox/draft/vr1-office1-current-*.json) +EOF + exit 2 +} + +case "$ACTION" in check) ;; *) usage ;; esac +case "$SITE" in + vr1-dc0|vr1-dc1) ;; + dc0|dc1|dc2) + echo "FAIL: bare '$SITE' is RETIRED (D-119) -- it was AMBIGUOUS ACROSS REGIONS." >&2 + echo " Use the region-qualified selector: vr1-dc0 | vr1-dc1" >&2 + exit 2 ;; + *) usage ;; +esac + +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); printf ' [ok] %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' [FAIL] %s\n' "$1"; } +refuse() { echo; echo "REFUSE: $1" >&2; echo " (could not evaluate -- this is NOT a pass)" >&2; exit 2; } + +# ---- D-134 AMENDMENT 2026-07-23 band table (the authoritative one) ------------ +# Host-part offsets within each plane. Row 0 of every VR1 DC plane, BOTH DCs. +# .1-.3 infrastructure (.1 gw, .2 rack, .3 DNS forwarder on metal-admin) +# .4-.49 RESERVED utility/infra nodes +# .50-.99 VIP band (widened from .50-.60 by the R11 D-020 amendment) +# .100-.200 nodes (control .100-.119 / compute .120-.149 / storage .150-.200) +# .201-.254 dynamic (MOVED here from .100-.200 by the amendment) +BAND_UTIL_LO=4; BAND_UTIL_HI=49 +BAND_VIP_LO=50; BAND_VIP_HI=99 +BAND_DYN_LO=201; BAND_DYN_HI=254 + +# ---- v4 planes from lib-net (site-keyed) ------------------------------------- +# shellcheck disable=SC1091 +source "$REPO/scripts/lib-net.sh" >/dev/null 2>&1 || refuse "cannot source scripts/lib-net.sh" +lib_net_select_dc "$SITE" >/dev/null 2>&1 || refuse "lib_net_select_dc '$SITE' failed" +[ "${#PLANE_CIDRS[@]}" -gt 0 ] || refuse "lib-net produced no PLANE_CIDRS for $SITE" + +# ---- v6 planes from the APEX record ------------------------------------------ +APEX_RECORD="${APEX_RECORD:-$(ls -1t "$REPO"/netbox/draft/vr1-office1-current-*.json 2>/dev/null | head -1)}" +[ -n "$APEX_RECORD" ] && [ -r "$APEX_RECORD" ] \ + || refuse "no readable apex record (looked for netbox/draft/vr1-office1-current-*.json; set APEX_RECORD)" + +V6_MAP="$(python3 - "$APEX_RECORD" "$SITE" <<'PY' +import json, sys +rec, site = sys.argv[1], sys.argv[2] +try: + d = json.load(open(rec)) +except Exception as e: + sys.stderr.write("apex record unreadable: %s\n" % e); sys.exit(3) +out = [] +for p in d.get('ipam/prefixes', []): + pre = p.get('prefix', '') + if ':' not in pre or not pre.endswith('/64'): + continue # /60 and /48 are parent blocks, not planes + sc = p.get('scope') or {} + if (sc.get('name') or p.get('scope_site') or '') != site: + continue + role = p.get('role') or {} + role = role.get('slug') if isinstance(role, dict) else role + desc = (p.get('description') or '') + # the provider VIP /64 is a SEPARATE block from the provider node /64; it is + # not a node-addressing plane, so it is reported apart rather than as a plane. + kind = 'vip' if 'VIP' in desc else 'plane' + out.append("%s\t%s\t%s" % (role, kind, pre)) +print("\n".join(sorted(out))) +PY +)" || refuse "could not parse the apex record" +[ -n "$V6_MAP" ] || refuse "apex record carries NO /64 prefixes scoped to $SITE" + +# ---- live MAAS --------------------------------------------------------------- +command -v maas >/dev/null 2>&1 \ + || refuse "the 'maas' client is ABSENT on this host. NOTE: a missing binary is a + MISSING TOOL, not an unreachable MAAS -- the 2026-07-27 audit found three separate + preflight/deploy failures that were all 'the client is not installed' reported as + something else. Run this from the D-128 Plane-2 host (voffice1)." + +SUBJSON="$(maas "$MAAS_PROFILE" subnets read 2>/dev/null)" \ + || refuse "'maas $MAAS_PROFILE subnets read' failed -- MAAS unreachable or profile '$MAAS_PROFILE' not logged in" +RANGEJSON="$(maas "$MAAS_PROFILE" ipranges read 2>/dev/null)" \ + || refuse "'maas $MAAS_PROFILE ipranges read' failed" + +echo "== dc-plane-ipam check $SITE ==" +echo " apex record: ${APEX_RECORD#$REPO/}" +echo +echo "-- v4 planes (from lib-net) --" +for c in "${PLANE_CIDRS[@]}"; do + name="${PLANE_NAME[$c]:-?}" + fab="$(printf '%s' "$SUBJSON" | python3 -c " +import json,sys +c=sys.argv[1] +for x in json.load(sys.stdin): + if x['cidr']==c: print((x.get('vlan') or {}).get('fabric','?')); break +else: print('') +" "$c")" + if [ -n "$fab" ]; then ok "v4 $name $c present (fabric=$fab)" + else bad "v4 $name $c ABSENT from MAAS"; fi +done + +echo +echo "-- v6 planes (from the apex) --" +while IFS=$'\t' read -r role kind pre; do + [ -z "$pre" ] && continue + present="$(printf '%s' "$SUBJSON" | python3 -c " +import json,sys +p=sys.argv[1] +print('yes' if any(x['cidr']==p for x in json.load(sys.stdin)) else 'no') +" "$pre")" + if [ "$kind" = "vip" ]; then + # Not asserted as a MAAS subnet: the GUA VIP /64 holds hacluster-managed API + # VIPs, not node addresses, so MAAS never allocates from it and cannot hand + # one out. Reported for visibility; whether it should ALSO be a MAAS subnet + # is an open design question, deliberately not decided by this gate. + printf ' [note] %-16s %-30s provider VIP block, MAAS subnet=%s (not asserted)\n' "$role" "$pre" "$present" + continue + fi + if [ "$present" = "yes" ]; then ok "v6 $role $pre present" + else bad "v6 $role $pre ABSENT from MAAS (R2 dual-stack is RULED; the v6 carve has not run)"; fi +done <<< "$V6_MAP" + +echo +echo "-- D-134 reserved bands (v4) --" +for c in "${PLANE_CIDRS[@]}"; do + name="${PLANE_NAME[$c]:-?}" + base="${c%.*}"; base="${base%.*}" # 10.12.4.0/22 -> 10.12 + third="$(printf '%s' "$c" | cut -d. -f3)" # third octet of the /22 base + for band in "util:$BAND_UTIL_LO:$BAND_UTIL_HI" "vip:$BAND_VIP_LO:$BAND_VIP_HI"; do + bn="${band%%:*}"; rest="${band#*:}"; lo="${rest%%:*}"; hi="${rest##*:}" + want_lo="$base.$third.$lo"; want_hi="$base.$third.$hi" + got="$(printf '%s' "$RANGEJSON" | python3 -c " +import json,sys +lo,hi=sys.argv[1],sys.argv[2] +for r in json.load(sys.stdin): + if r.get('start_ip')==lo and r.get('end_ip')==hi: + print(r.get('type','?')); break +else: print('') +" "$want_lo" "$want_hi")" + case "$got" in + reserved) ok "$name $bn band $want_lo-$want_hi reserved" ;; + "") bad "$name $bn band $want_lo-$want_hi NOT reserved (D-134 is RULED; MAAS may hand these out mid-deploy)" ;; + *) bad "$name $bn band $want_lo-$want_hi exists but type='$got' (want 'reserved') -- an unrecognised state REFUSES rather than passing" ;; + esac + done +done + +echo +echo "RESULT: pass=$PASS fail=$FAIL" +if [ "$FAIL" -eq 0 ]; then + echo "PASS: dc-plane-ipam check $SITE" + exit 0 +fi +echo "FAIL: dc-plane-ipam check $SITE -- $FAIL assertion(s) failed" +exit 1