diff --git a/.claude/skills/openstack-cloud-ops/references/platform-traps.md b/.claude/skills/openstack-cloud-ops/references/platform-traps.md index 28e44ec..78dd56d 100644 --- a/.claude/skills/openstack-cloud-ops/references/platform-traps.md +++ b/.claude/skills/openstack-cloud-ops/references/platform-traps.md @@ -466,6 +466,61 @@ --- +## 6. MAAS IPAM -- two behaviours that silently invert an expectation + +Both MEASURED 2026-07-27 while carving IPv6 into the VR1 DC planes. + +### 6a. MAAS ALREADY reserves the whole low IPv6 block on every v6 subnet + +Creating an explicit reserved iprange inside a v6 subnet FAILS: + +``` +{"start_ip": ["Requested reserved range conflicts with an existing range."], + "end_ip": ["Requested reserved range conflicts with an existing range."]} +``` + +...and `maas ipranges read` shows **zero** v6 ranges, so the "existing range" +is invisible there. It is MAAS's own default, visible only via: + +``` +maas subnet reserved-ip-ranges + :: - :: purpose ['rfc-4291-2.6.1'] <- subnet-router anycast + ::1 - ::ffff:ffff purpose ['reserved'] <- the whole first 2^32 +``` + +Allocatable space begins only at `:0:1::`. **CONSEQUENCE: a v4-style "reserve the +VIP/utility band" pass is both impossible AND unnecessary on IPv6** -- the addresses are +already protected, far more broadly than any band you would have written. Verify the +property instead of writing a range. This inverted a ruled execution plan (D-134's R4 +amendment sequenced "v6 bands as a second pass"); the ruling's INTENT was satisfied by +MAAS's default, and only the mechanism changed. + +### 6b. `mode=static` means EXPLICITLY CONFIGURED -- a subnet does not address anything + +Creating a subnet gives nodes NOTHING. Every address a node holds under `mode=static` was +written by an explicit `interface link-subnet ... ip_address=` call. Measured: after +carving 12 v6 plane subnets, all 18 Ready nodes still reported **zero IPv6 links**, while +their v4 links (set by an earlier carve) were all present and octet-mirrored. + +So "MAAS assigns the address" is only true if something assigned it. Ruling that node +addressing is "MAAS static assignment" does NOT make addresses appear -- it names the +mechanism, and the carve is still owed (18 nodes x 6 planes = 108 links here). Read +`interface_set[].links[]` before believing a subnet is in use. + +## NetBox -- the same object reads differently live vs in a dump + +`scope.name` from the LIVE API is the DISPLAY name; `scope.slug` is the key: + +``` +live: "scope": {"name": "VR1 DC0", "slug": "vr1-dc0", ...} +dump: "scope": {"name": "vr1-dc0"} <- this repo's dumpers NORMALISE name to the slug +``` + +A tool written against a dump and pointed at the live API matches **zero** objects. Match +`slug` first with `name` as fallback so both shapes work. Related lesson from the same +session: a tool that REFUSES on "found nothing" surfaces this immediately, where one that +treats zero matches as "nothing to do" would have written nothing and reported success. + ## Verbatim error -> cause index (grep this first) | Exact string you see | Cause | Where | diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 07401b0..a046b98 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1337,6 +1337,33 @@ read from the node's own address) -- no plane table in the tool. **The gate discriminates rather than agreeing with whatever it finds: it flipped dc0 to PASS while dc1 still read FAIL, before dc1 was carved.** +- **OPEN QUESTIONS CARRIED FORWARD FROM THIS SESSION, recorded HERE because they existed only + in a session changelog** -- which is session-scoped scratch, explicitly NOT citable as status + or decision authority and consolidated away at stage close. A close-sweep found them: + 1. **WHICH HOST IS AUTHORITATIVE FOR THE GATES?** R10 ruled preflight belongs on voffice1; + R15(3) (now EXECUTED) makes preflight strict about unexpected exit codes. Together, and + given P0-2's host-blind P5, **preflight on voffice1 is now permanently unpassable** -- its + 34 findings there are host artifacts, not defects. R15(2)'s manifest likewise does not + reach P0-1 (the gauntlet's `tofu fmt` walks the filesystem, so voffice1 stays red on a + gitignored file only it has). Needs one GA-R5 exchange; no D-number self-assigned, GA-R3 + resolves doubt DOWN to OPS. + 2. **THE `provider-bundle-check` ARITY GAP** -- `:137` requires exactly 3 addresses per + `vip`, a dual-family VIP is 6, so under R2 the checker fails EVERY application; `:149`'s + octet extraction also returns the whole string on a v6 literal. R11 ruled three gate + changes and NOT this one. **Now imminent rather than theoretical: the apex holds the + dual-family set the renderer will emit, so the gate cannot validate what is about to be + produced.** Also carried: `EXPECT_PUBLIC_VIP` must STAY 11 while `VIP_COUNT_EXPECT` goes + to 13, because neither vault nor designate has a `public` binding. + 3. **voffice1 TRACKS `dc-dc-stage5-preconditions`, not `main`** -- required, since the + repo-carried tooling runs where `maas` lives. **Return it to `main` at merge**; a working + host left on a retired branch is exactly the Phase-0 defect this session opened by fixing. +- **THREE PLATFORM BEHAVIOURS GRADUATED to `references/platform-traps.md`** at session close, + having been recorded only in this status document (which is consolidated over time, so a + durable trap does not belong here alone): MAAS auto-reserves `::1`-`::ffff:ffff` on EVERY + IPv6 subnet so an explicit v6 band write is impossible and unnecessary; MAAS `mode=static` + means EXPLICITLY CONFIGURED, so creating a subnet addresses nothing and the carve is still + owed; and NetBox's LIVE API returns `scope.name` as the display name while this repo's dumps + normalise it to the slug, so a dump-written tool matches zero objects live. - **RENDER-PIPELINE STEP 3 -- MAAS, lib-net AND APEX HALVES COMPLETE 2026-07-27.** Every authoritative source now carries the ruled values: **MAAS** (12 v6 plane subnets carved; 24 D-134 bands + both FIP pools reserved; `dc-plane-ipam check` pass=24 fail=0 and `reserve` planned=0 on BOTH DCs), **`lib-net.sh`** diff --git a/netbox/dc-plane-apex-import.py b/netbox/dc-plane-apex-import.py index dbcdd0d..491d52a 100644 --- a/netbox/dc-plane-apex-import.py +++ b/netbox/dc-plane-apex-import.py @@ -35,6 +35,7 @@ import subprocess import sys import urllib.error +import urllib.parse import urllib.request UA = "curl/8.5.0" # upstream 403s the default python UA -- platform-traps.md @@ -252,5 +253,4 @@ if __name__ == "__main__": - import urllib.parse # noqa: E402 (kept local; parse used only in main) sys.exit(main())