diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3ce597b..868a5d5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -35,6 +35,8 @@ > D-101/D-115/D-124/D-134 and confirm `10.13.0.0/16` is LIVE-free (existing cloud + tailnet). Draft > mapping (octet-preserving `10.12.a.b -> 10.13.a.b`): `docs/audit/netbox-1013-subnetting-draft-20260808.md`. > Sweep: `docs/audit/queued-findings-20260808-dc0-tailscale-incident-reip-pivot.txt`. +> Ruling-prep package (drafted GA-R5 question + D-101/115/124/134 reconciliation + measured blast +> radius + the 3 OWED live-free checks): `docs/audit/reip-1013-ga-r5-ruling-prep-20260808.md` (2026-08-08). > > **dc0 checkpoint scope (operator 2026-08-08): "activate + smoke-test"** -- networks + Octavia (1 test > LB) + Designate (1 test zone) + wrap gates (cloud-assert BOM, controller backup, verify-live diff --git a/docs/audit/reip-1013-ga-r5-ruling-prep-20260808.md b/docs/audit/reip-1013-ga-r5-ruling-prep-20260808.md new file mode 100644 index 0000000..7d13107 --- /dev/null +++ b/docs/audit/reip-1013-ga-r5-ruling-prep-20260808.md @@ -0,0 +1,340 @@ +# GA-R5 ruling PREP -- VR1 re-IP 10.12.0.0/16 -> 10.13.0.0/16 + +**PREP PACKAGE, NOT A RULING.** This file does not rule, does not mint a D-number, and +mutates nothing (no live infra, no `lib-net.sh`, no `design-decisions.md`, no +`CURRENT-STATE.md`). It exists so the operator can rule the re-IP from one place with the +full blast radius, the decision reconciliation, and the owed live checks in view. + +- Author: background agent (ruling-prep task), 2026-08-08. READ-ONLY session. +- Inputs read in full: `docs/audit/netbox-1013-subnetting-draft-20260808.md` (the 10.13 + subnetting draft), `docs/audit/queued-findings-20260808-dc0-tailscale-incident-reip-pivot.txt` + (F1, F12-F16), `docs/design-decisions.md` D-101/D-115/D-124/D-134 (each block in full), + `scripts/lib-net.sh` (defaults + the three selector arms). +- Method: document analysis + read-only repo grep. Every count in Section E is a measured + `grep` this session, not an estimate. No operational command was improvised (tool-index + consulted; the checks that need live/control-plane access are listed as OWED, Section D). +- **Next-free D-number: D-143** (verified: D-142 is the highest ASSIGNED entry, PROPOSED, + `docs/design-decisions.md:7966`; every `D-143` hit in the repo is "next-free" bookkeeping, + not an assignment). **NOT minted here** -- grep next-free again at ruling time. + +--- + +## A. Decision reconciliation + +For each governing decision: what it ruled, how the re-IP interacts with it, and the verb +(supersedes / amends / terminates / consistent-with). **One framing in this section +DIVERGES from the draft and from F12/F13 -- flagged inline so the operator can overrule it.** + +### D-101 -- DC-DC address plan and IPv6 family matrix -> **TERMINATES (one clause)** + +D-101 (ADOPTED 2026-07-09, `docs/design-decisions.md:2243`) rules the per-DC v4 layout and +the IPv6 family matrix. The load-bearing v4 clause, verbatim: + +> "Per-DC distinct v4: DC1 INHERITS the DC0 six-plane v4 layout (provider-public +> 10.12.4.0/22 ... replication 10.12.36.0/22) unchanged, so DC1 equals the validated +> template." (`docs/design-decisions.md:2304`; per the DOCFIX-205 annotation at :2247, in +> this 2026-07-09 text "DC1" = VR1's first DC = `vr1-dc0` and the inherited "DC0" = VR0's +> DC0 = `vr0-dc0`, the LIVE cloud.) + +**Interaction:** the live 10.12 cloud IS `vr0-dc0`. Moving VR1 to 10.13 means `vr1-dc0` no +longer inherits `vr0-dc0`'s v4 layout -- it gets a distinct, non-overlapping one. That is +the **termination of D-101's inherit-VR0-DC0-v4-unchanged clause** (F13). Note the *reason* +D-101 gave for inheritance (":2304" -- "minimize delta to Roosevelt", reuse the validated +bundle) is what the re-IP trades away, so the ruling should acknowledge the cost, not just +the mechanics. The IPv6 family matrix, the "IPv6 unless IPv4 is necessary" standing +principle (:2282), and the dual-stack ruling are **untouched** -- this is a v4-only re-IP. +**Verb: TERMINATES the v4-inheritance clause; CONSISTENT-WITH everything else in D-101.** + +### D-115 -- VR1 Office1 addressing / the v4 role carve -> **AMENDED (a factual premise), NOT reversed** + +D-115 (ADOPTED 2026-07-13, `docs/design-decisions.md:3711`; amendment at :3827) ruled: v4 +stays **role-based**; offices get a `/22`; and DC2 (`vr1-dc1`) moves INSIDE the Cloud `/16` +at `10.12.64.0/19`. The clause that names 10.13, verbatim: + +> "This SUPERSEDES `docs/dc-dc-netbox-buildout-scope.md`'s `DC2_V4_SUPERNET = 10.13.0.0/19`, +> which sat outside every allocated block." (`docs/design-decisions.md:3856-3858`) + +and the finding it rested on: + +> "DC2's planned `10.13.0.0/19` is OUTSIDE the Cloud `/16`. ... NetBox's Cloud role is +> `10.12.0.0/16`; `10.13.x` is unallocated." (`docs/design-decisions.md:3766-3767`) + +**Interaction -- and the framing divergence.** F12 and the draft (OQ (c)) call the re-IP a +"D-115 **SUPERSESSION**." I read the text more narrowly and flag it for operator override: +D-115's **ruling** (v4 role-based; /22 per office; DC2 inside the Cloud /16) is NOT being +reversed -- the re-IP keeps v4 role-based and keeps the octet-preserving structure. What the +re-IP overturns is one **factual premise**: that "10.13.x is unallocated / outside every +allocated block" makes it unusable. Post-pivot, 10.13.0.0/16 is *deliberately* allocated +under the `10.0.0.0/8` Corp Private parent. So the defensible verb is **AMENDS D-115** (its +role-based principle holds; its 10.13-is-unallocated finding is superseded by the collision +discovery), not a wholesale supersession. If the operator prefers the draft's stronger word, +that is their call -- but the text at :3856 only supersedes a *scope-doc recommendation*, not +a rule, so "amendment" is what the record supports. **Verb: AMENDS D-115 (factual premise); +the role-based ruling is CONSISTENT-WITH the re-IP.** The apex fork this opens is Section B. + +### D-124 -- Office1-region <-> DC-rack management overlay -> **CONSISTENT-WITH (routes re-point)** + +D-124 (ADOPTED 2026-07-16, `docs/design-decisions.md:4961`; addressing pinned at :5019, dc1 +at :5039) rules **Scheme A**: a dedicated `transit` role on `172.31.0.0/24` (NOT under +Cloud), region<->rack `/30`s (`172.31.0.0/30` dc0, `172.31.0.4/30` dc1), and the rack +metal-admin statics (`10.12.8.2` dc0, `10.12.68.2` dc1). + +**Interaction:** the transit supernet `172.31.0.0/24` is **not in 10.12** and does not move +-- D-124's addressing stands. BUT the rack metal-admin statics ARE in 10.12 (`10.12.8.2` -> +`10.13.8.2`, `10.12.68.2` -> `10.13.68.2`; draft Section 3.7) and the **routes carried over +the transit /30s point at 10.12.* DC destinations and must re-point to 10.13.*** (draft +Section 6 caveat). Also note (:5036) the NetBox `--commit` for D-124 addressing runs ON +office1-netbox because the apex token is unreachable from vcloud -- same access limit that +makes the live-NetBox 10.13 free-check an OWED foreground step (Section D). **Verb: +CONSISTENT-WITH; transit addresses stay, rack statics shift by the 12->13 rule, transit +ROUTES re-point.** + +### D-134 -- VR1 DC node static addressing (per-role octet bands) -> **CONSISTENT-WITH (survives unchanged)** + +D-134 (ADOPTED 2026-07-23, `docs/design-decisions.md:5858`; contiguous-band amendment at +:5900; the standing octet map at :5935 and :5984) rules the per-plane last-octet bands +(`.4-.49` utility, `.50-.99` VIP, `.100-.200` nodes, `.201-.254` dynamic) and the STANDING +cross-DC utility octet map (`.4` artifact / `.5` juju / `.6` MAAS region / `.7` tailscale). +The load-bearing half, verbatim: + +> "THE OCTET MAP IS A STANDING CROSS-DC STANDARD. ... They FOLLOW ALL DC DEPLOYMENTS so +> that standardized configuration is upheld through multiple datacenter stand ups." +> (`docs/design-decisions.md:5965-5968`) + +**Interaction:** D-134's bands and octet map are **offset-relative to the plane /22**, not +tied to the second octet. `10.12.8.5` -> `10.13.8.5` preserves "juju = .5" exactly. The +re-IP therefore **preserves D-134 by construction** -- and the standing cross-DC rule (":a +new DC standup reads the same table") is precisely what makes an octet-preserving shift the +natural mapping. This is a POSITIVE: D-134 survives the re-IP unchanged and actively argues +FOR the 1:1 shift. **Verb: CONSISTENT-WITH (survives unchanged; endorses the octet-preserving +map).** + +**Summary of verbs:** D-101 TERMINATES (v4-inheritance clause only) | D-115 AMENDS (factual +premise; role-based ruling holds) | D-124 CONSISTENT (routes re-point) | D-134 CONSISTENT +(survives, endorses the map). + +--- + +## B. The core question the ruling must answer (from F12): the IPAM apex fork + +The live cloud occupies `10.12.0.0/16` (NetBox **Cloud** role) and, per the operator's +standing constraint ("never editing live infra"), it STAYS there untouched. The re-IP +therefore does not "move a subnet"; under the plain reading of D-115 it **creates +`10.13.0.0/16` as a NEW allocation under the `10.0.0.0/8` Corp Private parent** (which +already covers 10.13 -- draft Section 4 confirms the `10.0.0.0/8` aggregate needs no change). +The fork is *where that new /16 lands in the role model*: + +**Option B1 -- Cloud role gains 10.13.0.0/16 as a SECOND prefix (10.12 retained as vr0-dc0's record).** +- One role, two /16 prefixes: 10.12 (VR0/live) + 10.13 (VR1 rebuild). +- Tradeoff: cheapest role-model change; keeps everything "Cloud" under one role, matching + D-115's role-based intent. But two /16s under one role blurs which is live-VR0 vs + rebuild-VR1 unless prefix descriptions carry it. No live record is re-labelled. + +**Option B2 -- NEW role (e.g. "Cloud -- VR1 rebuild") owns 10.13.0.0/16; Cloud stays 10.12-only.** +- Two roles: `Cloud` (10.12, VR0/live) + a new VR1-scoped role (10.13). +- Tradeoff: cleanest separation of the live cloud from the rebuild; makes the VR0/VR1 split + explicit in the apex (mirrors how D-115 seeded new `Office`/`Edge` roles and D-124 seeded + `transit`). Costs one new role + a small conceptual asymmetry (two "Cloud-ish" roles). No + live record is re-labelled. + +**Option B3 -- Cloud role MOVES to 10.13; 10.12 re-homed to a legacy/VR0 role.** +- `Cloud` becomes 10.13 (VR1); 10.12 is relabelled onto a new legacy/VR0 role. +- Tradeoff: **FLAG -- this re-labels a LIVE PRODUCTION record.** It is the option that most + directly rubs against the operator's "never edit live infra" constraint (the very + constraint driving the whole pivot). Not recommended for that reason; listed for + completeness. + +**Recommendation surfaced (operator decides):** B1 or B2 over B3. B2 if the apex should make +the VR0-live / VR1-rebuild split explicit (most defensible against "never edit live infra"); +B1 if minimizing role churn is preferred. B3 only if the operator explicitly wants "Cloud" +to always mean the current build and accepts touching a live record. + +--- + +## C. The GA-R5 ruling QUESTION drafted for the operator + +GA-R5 = one decision per exchange, must reconcile D-101/D-115/D-124/D-134, quote verbatim +when ruled. The re-IP actually contains **THREE** ruling-grade sub-questions. GA-R5's +one-decision-per-exchange rule means the operator may want to take them in sequence (C.1 +first -- it governs the others), or rule all three in one exchange if they prefer. Presented +so each can be quoted verbatim. + +### C.1 -- the apex fork (the load-bearing one; Section B) + +> "The VR1 rebuild moves off 10.12.0.0/16 (which collides with the live vr0-dc0 cloud) onto +> a newly-allocated 10.13.0.0/16 under the 10.0.0.0/8 Corp Private parent. The live cloud +> stays on 10.12, untouched. In the NetBox IPAM apex, does 10.13.0.0/16: (B1) join the +> existing Cloud role as a second prefix alongside 10.12; (B2) become a NEW role +> ('Cloud -- VR1 rebuild') with Cloud staying 10.12-only; or (B3) become the Cloud role, +> with 10.12 re-homed to a legacy/VR0 role? This AMENDS D-115 (its v4-role-based ruling +> holds; its finding that 10.13.x is unallocated is superseded by the collision discovery), +> TERMINATES D-101's 'VR1 DC0 inherits VR0 DC0's v4 layout unchanged' clause, is +> CONSISTENT-WITH D-124 (172.31 transit stays; routes re-point) and CONSISTENT-WITH D-134 +> (the octet bands/map survive unchanged)." + +### C.2 -- the mapping rule (draft OQ (b)) + +> "Adopt the octet-preserving 10.12.a.b -> 10.13.a.b shift for the entire VR1 Cloud DC space +> (both DCs, second octet 12->13, octets 3/4 unchanged), preserving the six-plane layout, +> dc1's /19 supernet, the D-134 octet map, the VIP columns, and the FIP pool offsets? Or use +> this greenfield /16 to regularize dc0's non-contiguous plane offsets (4/8/12/16 then +> 32/36) into a contiguous /19-shaped block symmetric with dc1 -- at the cost of breaking +> octet-preservation and diverging further from the Roosevelt/VR0 template?" + +Recommendation surfaced: the 1:1 octet-preserving shift (C.2 first option). D-134's standing +cross-DC octet map (:5965) actively endorses it, and it minimizes the blast-radius diff. + +### C.3 -- the lib-net.sh structural fork (NEW -- not in the draft, not in F13) + +**This sub-question is surfaced by my read of `scripts/lib-net.sh:22-83`, which the draft did +not carry.** The file's flat defaults (`PLANE_CIDRS`, `PLANE_ROLES`, `PLANE_GW` incl. +`10.12.8.1`, `VIP_PREFIX_*`, `FIP_POOL_*`, `KEYSTONE_VIP_DEFAULT`) **ARE vr0-dc0's real +as-built values** -- the file says so at line 37: *"10.12.8.1 is VR0's REAL as-built value +(phase-00-maas-standup.sh:119)"*. The `vr0-dc0` selector arm is a documented **no-op** over +these defaults (:119-121); the `vr1-dc0` arm currently *inherits* them and overrides only +`PLANE_GW` (drops the metal-admin router) and unsets the VID (:123-159). So the re-IP is not +"edit the literals" -- it is a **structural inversion of the file**, and which shape it takes +is a ruling-grade choice: + +> "In scripts/lib-net.sh the flat defaults ARE vr0-dc0's live as-built values and the +> vr0-dc0 arm is a no-op over them. After the re-IP: (i) keep the flat defaults at 10.12 +> (= vr0-dc0 live) and give BOTH VR1 arms (vr1-dc0, vr1-dc1) their own complete 10.13 literal +> blocks; or (ii) make the flat defaults 10.13 (VR1 is the forward build) and give vr0-dc0 a +> full explicit 10.12 override arm?" + +- **(i)** preserves the invariant line 37 documents -- "the defaults are the live cloud's + measured values" -- which keeps the vr0-dc0 no-op honest. More code in the VR1 arms. +- **(ii)** reads more naturally for a repo whose future IS VR1, but **silently re-purposes + the defaults away from the live cloud's measured values, breaking line 37's invariant**. + +Either way, the comment at `scripts/lib-net.sh:124-134` ("Same PLANE values as vr0-dc0 ... +INHERITS VR0 DC0's v4 layout UNCHANGED") becomes FALSE and owes an update in the same change +(F13). NOT recommended for the operator here beyond noting (i) keeps the documented +invariant and (ii) breaks it. + +--- + +## D. Live-free check status + +Separated: **verified read-only by me** vs **OWED live/control-plane checks** (F16(a)). + +### Verified read-only by me (repo-only evidence) + +- **10.13 appears NOWHERE as a LIVE cloud allocation in the repo.** Every in-repo `10.13` + hit (full grep, excl `.git`) is one of: (1) this pivot's own artifacts (the draft, the + findings, CURRENT-STATE's pivot banner, session-ledger); (2) **historical/superseded** + references to the old `10.13.0.0/19` DC2 default (`docs/archive/...`, + `docs/dc-dc-netbox-buildout-scope.md:158,190`, `docs/design-decisions.md:3766/3797/3809/3856`); + (3) **stale/test literals** (see the collision hazard below). None is a live-object record. +- **Repo greps confirm the blast radius** (Section E) -- measured, not estimated. +- **CAVEAT (carried from the draft, keep it):** the draft's 10.13 scan was against + `netbox/draft/vr1-office1-current-20260801.json`, a DATED SNAPSHOT. Absence there is NOT a + measured LIVE absence. The 10.12 collision that caused this whole pivot was found by + discovery at Headscale, not by any scan -- so 10.13 must be proven free the same way, + LIVE, before any dependent work. + +**Naming-collision hazard I verified (feed into the ruling / a DOCFIX):** 10.13 already +carries TWO pre-existing meanings in-repo before this re-IP mints a third: +- `netbox/README.md:49` -- stale importer default `VR1_DC1_V4_SUPERNET=10.13.0.0/19` + (pre-D-115; D-115 moved dc1 to `10.12.64.0/19`). Draft Section 5 flags this. +- `tests/dc-rack-mgmt-import/test_logic.py:265` -- uses `10.13.0.5` as a live test literal + (`--rack-ip`); mirrored in `docs/archive/stage3-review-base.patch:3667`. **The draft + MISSED this test-fixture use.** Reusing 10.13.0.0/16 as the rebuild /16 will make a rack-IP + test literal fall inside the new Cloud space -- reconcile so the three 10.13 meanings do + not confuse. + +### OWED live / control-plane checks (I do NOT have access; do NOT run blind) + +Per `docs/tool-index.md` discipline, I name the check AND whether a tested artifact exists. +None of the three below has a tool that answers "is this /16 in use," so each is documented +as a procedure, not improvised: + +1. **Headscale advertised/approved route table (OWED, foreground/operator-only).** No + artifact in `docs/tool-index.md` covers Headscale route enumeration; it is control-plane + access I do not have. This is the exact overlap class that caused the pivot. Check: list + the advertised + approved routes on the tailnet and confirm **no route overlaps + 10.13.0.0/16** (Headscale has no 4via6; routes must be non-overlapping). Also confirm the + dc0 `.7` subnet router's UNAPPROVED `10.12.8.0/22` advert (findings F1 / already-on-surface) + is not later approved onto a 10.13 conflict. +2. **Live NetBox apex (office1-netbox, `http://10.10.1.10:8000`) -- OWED, operator token.** + The apex token is operator-held and unreachable from vcloud (D-124 amendment, + `docs/design-decisions.md:5036`; also findings F11). Check (read-only): query prefixes / + aggregates for any object under `10.13.0.0/16`; confirm the /16 is free and that adding it + under `10.0.0.0/8` collides with nothing. Run on office1-netbox where the token lives. +3. **Live vr0-dc0 cloud + MAAS -- OWED.** The 10.12 collision was found by discovery, not a + scan. Symmetric read-only check owed on 10.13: confirm no live MAAS subnet / neutron + provider net / route / DHCP range anywhere touches 10.13.0.0/16. `scripts/dc-egress-check.sh` + and `scripts/maas-profile-assert.sh` exist but neither answers "is this /16 in use" -- so + this is a **tooling gap**: if the operator wants it repeatable, propose a small read-only + "is-this-prefix-free" checker rather than improvising `maas ... subnets read` grep at the + prompt. + +--- + +## E. Blast radius (measured greps this session, 2026-08-08) + +`10.12.` literal references, **excluding frozen history** (`asbuilt/`, `docs/audit/`, +`docs/archive/`, `.git/`). Counts are `grep -rl` (files) and `grep -rn` (line hits). + +| Surface | Files | Line hits | Change class | Key members | +|---|---:|---:|---|---| +| `scripts/` | 28 | 182 | **MUST CHANGE** | `lib-net.sh` (source of truth, 47 hits), `phase-00-maas-standup`, `dc-rack-net`, `dc-snap-proxy`, `provider-bundle-check.py` (independent octet band) | +| `tests/` | 69 | 566 | **MUST CHANGE** (twins + fixtures) | `dc-selector` (re-parses overlay), `provider-bundle-check`, `render-baseline` fixtures, `phase-04` fixtures | +| `netbox/` | 11 | 222 | **MUST CHANGE** | `dc-dc-prefixes-import`, `dc-plane-apex-import`, `d115-office-carve`, importers + draft JSONs | +| `runbooks/` | 22 | 144 | **MUST CHANGE** | procedure text citing plane CIDRs | +| `opentofu/` | 5 | 31 | **MUST CHANGE** | `variables.tf` (`vr1_dc1_planes` -- lib-net.sh's named twin), per-DC substrate `main.tf` | +| `overlays/` | 3 | 27 | **MUST CHANGE** (ratified VIP source) | `vr1-dc0-vips.yaml`, `vr1-dc1-vips.yaml`, `vr1-dc0-machines.yaml` | +| `render/` | 2 | 6 | **MUST CHANGE** | `render/values/vr1-dc{0,1}-vips.yaml` | +| `bundle.yaml` | 1 | 13 | **MUST CHANGE** | deploy input (VIPs) | +| `docs/` (excl audit+archive) | 31 | 549 | **MUST NOT REWRITE** | `design-decisions.md`, `CURRENT-STATE.md` -- prose record; D-117 ruled treatment = ANNOTATE in place / new dated entries, NOT rewrite | + +**Total live-surface `10.12` hits (excl asbuilt/audit/archive/.git): 3765 across 326 files.** +Read that number correctly: the ~549 `docs/` prose hits are the historical record and are +**not** a remap target (D-117: annotate, do not rewrite) -- the actual code/config remap work +is the eight MUST-CHANGE rows above (~1191 hits). Do not read 3765 as the work estimate. + +**Twin-file coupling (must change in the SAME commit, per CLAUDE.md delivery discipline):** +`lib-net.sh` <-> `opentofu/variables.tf` (`vr1_dc1_planes`), and `lib-net.sh` <-> +`overlays/*-vips.yaml` (via `tests/dc-selector`). Each script change ships its +`tests//run-tests.sh` green + `repo-lint` clean + a changelog entry with a revert. + +**NetBox object counts (from the draft, `vr1-office1-current-20260801.json`):** 13 prefixes, +24 ip-ranges, 80 ip-addresses (78 VIP legs + 2 rack `.2` statics), 0 aggregate changes +(`10.0.0.0/8` already covers 10.13). Plus 2 FIP pools that are **MAAS reserved ipranges, not +NetBox objects** (record gap, Section F). + +**Held (not in 10.12; do NOT remap):** `172.31.0.0/24` transit (routes re-point), `10.10.x` +office (D-115), `172.30.0.0/16` edge (D-115), `10.16/10.17` dev clouds, all IPv6 +(`2602:f3e2::/36`, `fd50:840e:74e2::/48`), the `10.0.0.0/8` aggregate. + +--- + +## F. Open questions the draft leaves (F16(b),(c)) surfaced for the operator + +1. **1:1 shift vs. regularize dc0 while greenfield (draft OQ (b) / F16(b)).** The octet + shift preserves dc0's non-contiguous offsets (`4/8/12/16` then `32/36`, skipping + `20/24/28`) and the dc0-vs-dc1 asymmetry (dc1 fits a /19; dc0 does not). Greenfield is the + only cheap moment to regularize -- at the cost of octet-preservation and further Roosevelt + divergence. Drafted as C.2. +2. **The lib-net.sh flat-defaults fork (NEW, Section C.3).** Not in the draft or F13 -- + surfaced from `scripts/lib-net.sh:22-83,37`. Needs a ruling on whether the defaults stay + 10.12 (live vr0-dc0, invariant preserved) or become 10.13 (VR1 forward, invariant broken). +3. **Record gaps to reconcile at ruling time (draft Section 4 / F15):** + - FIP pools are MAAS reserved ipranges, NOT NetBox objects (asserted by + `scripts/phase-04-network-verify.sh`) -- standing NetBox record gap. + - The dc1 `/19` supernet has **no** NetBox prefix object (only its six child /22s). + - Stale `10.13.0.0/19` importer default at `netbox/README.md:49` (pre-D-115). + - **(added by me)** `tests/dc-rack-mgmt-import/test_logic.py:265` uses `10.13.0.5` as a + rack-IP test literal -- a third 10.13 meaning; reconcile to avoid confusion. +4. **Framing to confirm/overrule (Section A, D-115).** I framed the re-IP as **AMENDING** + D-115's factual premise rather than the draft's / F12's "SUPERSESSION." The record at + `docs/design-decisions.md:3856` supersedes only a scope-doc *recommendation*, not a rule; + the operator should confirm "amendment" or rule "supersession" explicitly, since GA-R5 is + graded on this reconciliation. +5. **Owed non-ruling follow-ups (findings F1(b)):** `docs/CURRENT-STATE.md` pivot/checkpoint + update, and a GA-R5 ruling for the MAAS-topology-no-migration posture (F4) and the + checkpoint scope (F6) -- separate from this re-IP ruling. + +--- + +**PREP PACKAGE -- not ruled, not committed, no D-number minted.** diff --git a/docs/changelog-20260808-dc0-activation.md b/docs/changelog-20260808-dc0-activation.md new file mode 100644 index 0000000..7f35818 --- /dev/null +++ b/docs/changelog-20260808-dc0-activation.md @@ -0,0 +1,91 @@ +# Changelog 2026-08-08 -- dc0 activation (checkpoint) + re-IP ruling prep + +Session body (GA-R2, one per session). Branch `dc-dc-stage5-preconditions`. +Status authority is `docs/CURRENT-STATE.md` ONLY; this file is the review surface +(blanket-approval: every item states what / why / how-to-revert). + +Mission: drive dc0 to FULL deployment as a CHECKPOINT (STANDING PIVOT 2026-08-08), +then teardown + redeploy on 10.13.0.0/16. Scope this session: dc0 activation +(networks -> Octavia 1-LB -> Designate 1-zone -> wrap gates), plus a parallel +re-IP GA-R5 ruling-prep package. + +--- + +## Item 1 -- DOCFIX-213: phase-04 network scripts made MAAS_PROFILE-aware (F3/D-138 co-location fix) + +**What.** `scripts/phase-04-network-create.sh` and `scripts/phase-04-network-verify.sh` +now honour `MAAS_PROFILE="${MAAS_PROFILE:-admin}"` (matching the established +`carve-host-interfaces.sh` / `maas-role-tags.sh` idiom) instead of hardcoding +`maas admin`. Three `maas admin ...` call sites became `maas "$MAAS_PROFILE" ...` +(create:41 subnets; verify:57 subnets, verify:88 ipranges) plus the verify +precondition error message. Header "Requires" comments updated. The `admin` +default preserves VR0 single-DC behaviour unchanged; VR1 overrides to the DC +regional (e.g. `MAAS_PROFILE=vr1-dc0-region`). + +**Why.** F3 (queued-findings-20260808): the dc0 rack `vvr1-dc0` carries the +`openstack` CLI + admin-openrc + cloud L3, but an EMPTY maas profile db; the maas +profiles live on `voffice1` (no L3 to the dc0 cloud -- D-138). `phase-04-network-create.sh` +needs BOTH `maas` (DOCFIX-047 gateway cross-check by CIDR) AND `openstack` on ONE +host, so it could run on neither. MEASURED this session: the authoritative in-DC +regional `vr1-dc0-region` (running on dc0 node `hot-kid`, `10.12.8.6:5240`) carries +`10.12.4.0/22` gw `10.12.4.1`, and the rack reaches that API (HTTP 200 via +metal-admin `virbr2`). Operator directive (verbatim, 2026-08-08): **"each DC should +have its site regional maas and the racks register up to the DC regional +controller"** -- so the fix threads the DC-regional profile through rather than the +office1 `admin` default. The script change is durable and transfers to the 10.13 +rebuild; it does NOT strip the maas two-source gate (advisor: disarming it is the +instrument-#16 recurrence). + +**Tests.** Extended both existing harnesses (`tests/phase-04-create`, `tests/phase-04`) +with an `EXPECT_PROFILE`-keyed fake `maas` so a case can prove the SUT threads +`MAAS_PROFILE` through (not hardcoded `admin`). New cases: create "MAAS_PROFILE=vr1-dc0-region +passed through" (exit 0), verify "MAAS_PROFILE=vr1-dc0-region passed through" (PROCEED). +Failability PROVEN out-of-band: with `MAAS_PROFILE=vr1-dc0-region EXPECT_PROFILE=admin` +the create GATE-FAILs (exit 1, empty gateway) and the verify hits precondition exit 2 +reporting `'maas vr1-dc0-region subnets read'` -- confirming the profile is really +threaded. Both harnesses ALL PASS; repo-lint 0 fail (1 legacy L1 warn). + +**Revert.** `git checkout HEAD -- scripts/phase-04-network-create.sh +scripts/phase-04-network-verify.sh tests/phase-04-create/ tests/phase-04/` restores +the hardcoded-`admin` scripts and the prior harnesses. Behaviour reverts to VR0-only. + +**OWED (not in this item -- live, operator-gated):** register the `vr1-dc0-region` +maas CLI profile ON the rack (credential one-shot, SEC-safe -- generated on hot-kid, +piped to `maas login`, never printed), then run `MAAS_PROFILE=vr1-dc0-region +scripts/phase-04-network-create.sh` on the rack. Task #1 remains OPEN until that runs. + +**Rebuild-plan finding (LOGGED, not executed):** the outer substrate hosts +`vvr1-dc0` / `vvr1-dc1` are currently MAAS rack controllers under the OFFICE1 `admin` +region, NOT their DC regionals -- exactly the gap the operator directive names. The +10.13 rebuild tooling should register each rack up to its in-DC regional at standup. + +--- + +## Item 2 -- Re-IP GA-R5 ruling-prep package (Task #6, background agent) + +**What.** `docs/audit/reip-1013-ga-r5-ruling-prep-20260808.md` created (read-only +analysis; no live mutation, no D-number minted). Reconciles D-101/D-115/D-124/D-134 +for the 10.12 -> 10.13 re-IP, drafts the GA-R5 ruling question + options, enumerates +the consumer blast radius, and lists the OWED live-free checks. + +**Why.** The re-IP is a D-115 interaction + terminates D-101 inheritance; it needs a +GA-R5 ruling before any dependent re-carve (Task #2, the 10.13 NetBox scope, is +blocked on it). Preparing the package in parallel keeps the operator's ruling +exchange fact-based. + +**Key content.** Live vr0-dc0 stays on 10.12 (never edited), so the re-IP CREATES +10.13 as a NEW allocation -> the ruling must decide whether the NetBox Cloud role +gains 10.13 (B1), a new "Cloud -- VR1 rebuild" role owns it (B2, agent-recommended), +or the Cloud role moves (B3). Decision verbs: D-101 TERMINATES (v4-inheritance), +D-115 AMENDS (factual premise, not wholesale supersession), D-124/D-134 CONSISTENT. +Blast radius (measured, excl. frozen history): ~3765 `10.12` hits / 326 files, ~1191 +in eight must-change code/config surfaces. Next-free D verified D-143 (NOT minted). + +**OWED before the operator can rule (no read-only access from the agent):** +(1) Headscale advertised/approved route table -- no overlap with 10.13.0.0/16; +(2) live office1-netbox apex -- 10.13.0.0/16 free; (3) live vr0-dc0 MAAS/neutron -- +10.13 not in use. All three need the foreground/operator; a "is this /16 free" tool +is a tooling gap to propose rather than improvise. + +**Revert.** `git rm docs/audit/reip-1013-ga-r5-ruling-prep-20260808.md` (analysis +artifact only; nothing consumes it). diff --git a/scripts/phase-04-network-create.sh b/scripts/phase-04-network-create.sh index b92e654..6e3fe46 100644 --- a/scripts/phase-04-network-create.sh +++ b/scripts/phase-04-network-create.sh @@ -11,7 +11,10 @@ # the do-doc's hardcoded `maas admin subnet read 1`. # # Tunables via env (D-003 defaults): EXT_NET EXT_SUBNET PHYSNET FIP_START FIP_END -# Requires: jumphost; jq; admin-openrc sourced (OS_AUTH_URL); openstack + the 'admin' MAAS profile. +# Requires: jumphost; jq; admin-openrc sourced (OS_AUTH_URL); openstack + a usable MAAS +# profile. DOCFIX-213: the profile is MAAS_PROFILE (default 'admin' = VR0/office1); for +# VR1 the create runs ON the DC rack against the DC regional, so set +# MAAS_PROFILE=vr1-dc0-region (matches the carve-host-interfaces / maas-role-tags idiom). # Usage: source ~/admin-openrc && scripts/phase-04-network-create.sh # Exit: 0 created or already-present (gate confirmed) | 1 gate/confirm fail | 2 precondition # ASCII + LF. @@ -31,14 +34,15 @@ EXT_CIDR="${EXT_CIDR:-$PROVIDER_CIDR}" FIP_START="${FIP_START:-$FIP_POOL_START}" FIP_END="${FIP_END:-$FIP_POOL_END}" +MAAS_PROFILE="${MAAS_PROFILE:-admin}" # DOCFIX-213: VR1 multi-region -- override to the DC regional (e.g. vr1-dc0-region); 'admin' resolves to VR0/office1. need_jq || exit 2 command -v openstack >/dev/null 2>&1 || { echo "FAIL: openstack client not found" >&2; exit 2; } command -v maas >/dev/null 2>&1 || { echo "FAIL: maas client not found" >&2; exit 2; } [ -n "${OS_AUTH_URL:-}" ] || { echo "FAIL: OS_AUTH_URL unset -- 'source ~/admin-openrc' first" >&2; exit 2; } -# DOCFIX-047: provider gateway by CIDR (never 'maas admin subnet read 1') -GW=$(maas admin subnets read 2>/dev/null | jq -r --arg c "$PROVIDER_CIDR" '.[] | select(.cidr==$c) | .gateway_ip') +# DOCFIX-047: provider gateway by CIDR (never 'maas subnet read 1') +GW=$(maas "$MAAS_PROFILE" subnets read 2>/dev/null | jq -r --arg c "$PROVIDER_CIDR" '.[] | select(.cidr==$c) | .gateway_ip') [ "$GW" = "$GW_EXPECT" ] || { echo "GATE FAIL: MAAS provider gateway='$GW' (expected $GW_EXPECT, by CIDR $PROVIDER_CIDR)"; exit 1; } echo "[OK] gateway $GW (discovered by CIDR $PROVIDER_CIDR)" diff --git a/scripts/phase-04-network-verify.sh b/scripts/phase-04-network-verify.sh index d5e8952..a88ae64 100644 --- a/scripts/phase-04-network-verify.sh +++ b/scripts/phase-04-network-verify.sh @@ -15,7 +15,9 @@ # network external=true / type=flat / physnet1 / NOT shared (Option B isolation); # subnet cidr / gateway / no-dhcp / FIP allocation pool. # -# Requires: jumphost; jq; admin-openrc sourced (OS_AUTH_URL set); the 'admin' MAAS profile. +# Requires: jumphost; jq; admin-openrc sourced (OS_AUTH_URL set); a usable MAAS profile. +# DOCFIX-213: the profile is MAAS_PROFILE (default 'admin' = VR0/office1); for VR1 +# this runs ON the DC rack against the DC regional -- set MAAS_PROFILE=vr1-dc0-region. # NEVER runs 'maas list' (it prints the API key -- DOCFIX-016). # # Usage: source ~/admin-openrc && scripts/phase-04-network-verify.sh @@ -42,6 +44,7 @@ EXT_NET="provider-ext" EXT_SUBNET="provider-ext-fip" PHYSNET="physnet1" +MAAS_PROFILE="${MAAS_PROFILE:-admin}" # DOCFIX-213: VR1 multi-region -- override to the DC regional (e.g. vr1-dc0-region); 'admin' = VR0/office1. FATAL=0 fail() { echo "FAIL: $*" >&2; FATAL=$((FATAL + 1)); } @@ -53,10 +56,10 @@ command -v maas >/dev/null 2>&1 || { echo "FAIL: maas client not found" >&2; exit 2; } [ -n "${OS_AUTH_URL:-}" ] || { echo "FAIL: OS_AUTH_URL unset -- 'source ~/admin-openrc' first" >&2; exit 2; } -# MAAS 'admin' profile must be usable (read-only; NOT 'maas list' -- DOCFIX-016). -SUBNETS_JSON="$(maas admin subnets read 2>/dev/null || true)" +# MAAS profile ($MAAS_PROFILE) must be usable (read-only; NOT 'maas list' -- DOCFIX-016). +SUBNETS_JSON="$(maas "$MAAS_PROFILE" subnets read 2>/dev/null || true)" printf '%s' "$SUBNETS_JSON" | jq -e 'type=="array"' >/dev/null 2>&1 \ - || { echo "FAIL: 'maas admin subnets read' did not return JSON (profile 'admin' logged in?)" >&2; exit 2; } + || { echo "FAIL: 'maas $MAAS_PROFILE subnets read' did not return JSON (profile '$MAAS_PROFILE' logged in?)" >&2; exit 2; } echo "=== phase-04 network-carve verify (read-only) ===" echo @@ -85,9 +88,9 @@ # --- PRE C: FIP pool reserved on the provider subnet -------------------------------- echo "--- PRE: FIP pool $FIP_START-$FIP_END must be a RESERVED iprange on subnet $SID ---" -IPR_JSON="$(maas admin ipranges read 2>/dev/null || true)" +IPR_JSON="$(maas "$MAAS_PROFILE" ipranges read 2>/dev/null || true)" if ! printf '%s' "$IPR_JSON" | jq -e 'type=="array"' >/dev/null 2>&1; then - fail "'maas admin ipranges read' did not return JSON" + fail "'maas $MAAS_PROFILE ipranges read' did not return JSON" else echo " reserved ranges on subnet $SID:" printf '%s' "$IPR_JSON" | jq -r --argjson s "$SID" \ diff --git a/tests/phase-04-create/fakebin/maas b/tests/phase-04-create/fakebin/maas index 72e6f3d..036d2c1 100755 --- a/tests/phase-04-create/fakebin/maas +++ b/tests/phase-04-create/fakebin/maas @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# fake maas: 'admin subnets read' -> provider subnet with gateway $GW (default 10.12.4.1) -if [ "${1:-}" = admin ] && [ "${2:-}" = subnets ] && [ "${3:-}" = read ]; then +# fake maas: ' subnets read' -> provider subnet with gateway $GW (default 10.12.4.1). +# Responds ONLY when the invoked profile == EXPECT_PROFILE (default admin), so a test can prove +# the SUT passes MAAS_PROFILE through rather than hardcoding 'admin' (DOCFIX-213). +EXPECT_PROFILE="${EXPECT_PROFILE:-admin}" +if [ "${1:-}" = "$EXPECT_PROFILE" ] && [ "${2:-}" = subnets ] && [ "${3:-}" = read ]; then printf '[{"id":1,"cidr":"10.12.4.0/22","gateway_ip":"%s"}]\n' "${GW:-10.12.4.1}" fi diff --git a/tests/phase-04-create/run-tests.sh b/tests/phase-04-create/run-tests.sh index c201ddb..118ddb9 100644 --- a/tests/phase-04-create/run-tests.sh +++ b/tests/phase-04-create/run-tests.sh @@ -31,6 +31,10 @@ run 0 'SKIP. network provider-ext exists' "idempotent: both present" NET_PRESENT=1 SUB_PRESENT=1 run 1 'GATE FAIL: MAAS provider gateway' "gate fail: wrong gateway" GW=10.12.4.254 run 1 'CONFIRM FAIL' "confirm fail: shared=true" NET_PRESENT=1 SUB_PRESENT=1 BAD_SHARED=1 +# DOCFIX-213: MAAS_PROFILE is passed through (fails red if the SUT reverts to hardcoded 'admin': +# stub keyed to EXPECT_PROFILE=vr1-dc0-region would go silent -> GATE FAIL exit 1 != 0). +run 0 'network provider-ext created.*|create complete' "MAAS_PROFILE=vr1-dc0-region passed through" \ + MAAS_PROFILE=vr1-dc0-region EXPECT_PROFILE=vr1-dc0-region echo [ "$rc_all" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED" exit "$rc_all" diff --git a/tests/phase-04/fakebin/maas b/tests/phase-04/fakebin/maas index 752fdb8..1d78aab 100755 --- a/tests/phase-04/fakebin/maas +++ b/tests/phase-04/fakebin/maas @@ -1,10 +1,13 @@ #!/usr/bin/env bash -# fake maas: only 'maas admin subnets read' and 'maas admin ipranges read' are emulated, -# each emitting the env-pointed fixture. Anything else emits nothing. Never emulates -# 'maas list' (the real one prints the API key -- DOCFIX-016; the verify must not call it). +# fake maas: only ' subnets read' and ' ipranges read' are emulated, +# each emitting the env-pointed fixture, and ONLY when the invoked profile == EXPECT_PROFILE +# (default admin) -- so a test can prove the verify passes MAAS_PROFILE through (DOCFIX-213). +# Anything else emits nothing. Never emulates 'maas list' (the real one prints the API key -- +# DOCFIX-016; the verify must not call it). +EXPECT_PROFILE="${EXPECT_PROFILE:-admin}" prof="${1:-}"; obj="${2:-}"; act="${3:-}" -if [ "$prof" = "admin" ] && [ "$obj" = "subnets" ] && [ "$act" = "read" ]; then +if [ "$prof" = "$EXPECT_PROFILE" ] && [ "$obj" = "subnets" ] && [ "$act" = "read" ]; then cat "${FIX_SUBNETS:?FIX_SUBNETS not set}" -elif [ "$prof" = "admin" ] && [ "$obj" = "ipranges" ] && [ "$act" = "read" ]; then +elif [ "$prof" = "$EXPECT_PROFILE" ] && [ "$obj" = "ipranges" ] && [ "$act" = "read" ]; then cat "${FIX_IPRANGES:?FIX_IPRANGES not set}" fi diff --git a/tests/phase-04/run-tests.sh b/tests/phase-04/run-tests.sh index f22f966..f297e46 100644 --- a/tests/phase-04/run-tests.sh +++ b/tests/phase-04/run-tests.sh @@ -46,6 +46,12 @@ FIX_SUBNETS="$WORK/subnets-good.json" FIX_IPRANGES="$WORK/ipranges-good.json" \ FIX_NET="$WORK/net-good.json" FIX_SUBNET="$WORK/subnet-good-objpool.json" +# DOCFIX-213: MAAS_PROFILE passed through (fails red if the SUT reverts to hardcoded 'admin': +# stub keyed to EXPECT_PROFILE=vr1-dc0-region would go silent -> 'not JSON' precondition exit 2). +run 0 'PROCEED' "MAAS_PROFILE=vr1-dc0-region passed through" \ + MAAS_PROFILE=vr1-dc0-region EXPECT_PROFILE=vr1-dc0-region \ + FIX_SUBNETS="$WORK/subnets-good.json" FIX_IPRANGES="$WORK/ipranges-good.json" + # POST pass, allocation_pools as list-of-strings (tolerance proof) run 0 'PASS' "post pass (string pool)" \ FIX_SUBNETS="$WORK/subnets-good.json" FIX_IPRANGES="$WORK/ipranges-good.json" \