diff --git a/docs/changelog-20260716-d124-addressing-pin.md b/docs/changelog-20260716-d124-addressing-pin.md new file mode 100644 index 0000000..9a5d6a4 --- /dev/null +++ b/docs/changelog-20260716-d124-addressing-pin.md @@ -0,0 +1,65 @@ +# 2026-07-16 -- D-124 addressing pinned: transit supernet 172.31.0.0/24 + ratified rack values + +## Context + +The operator resolved the three scoping flags `netbox/dc-rack-mgmt-import.py` had surfaced (seed the +`transit` role; carve a dedicated transit supernet; prefix scope) and ratified the transit /30 + rack IP. +This pins the importer's `CONTAINER` and records the values. **NO apex mutation happened here** -- +office1-netbox (10.10.1.10) is UNREACHABLE from the vcloud jumphost (measured: ping/TCP:8000/HTTP all +fail, no SSH/tunnel), and the apex token lives on that host. The `--commit` is an on-host operator step. + +## Operator rulings (2026-07-16) + +1. Seed a NEW dedicated `transit` role (not the shared `infra` role). +2. **Carve** a dedicated transit supernet, NOT under Cloud `10.12.0.0/16`. Ratified **`172.31.0.0/24`** + (mirrors the Edge role's dedicated `172.30.0.0/16`; /24 = 64 /30s, ample for ptp transit links). +3. Transit prefix scope = `dcim.site:vr1-dc0` ("mirror the edge"). +4. Ratified values: transit /30 `172.31.0.0/30` (region/peer `.1`, rack `.2`); rack IP `10.12.8.2`. + +## What changed (repo-side; pushable) + +- **`netbox/dc-rack-mgmt-import.py`:** `CONTAINER = "10.12.0.0/16"` -> **`"172.31.0.0/24"`** (+ header + note: dedicated transit supernet, operator-pinned; scope-flag comment updated to RULED). Usage example + updated to the ratified values. The importer STILL takes `--transit-cidr`/`--rack-ip` as required inputs + (no invented literal) and still fail-loud on a missing/collision container. +- **`tests/dc-rack-mgmt-import/` (harness + `test_logic.py`):** the `CONTAINER` assertion + every transit + fixture rebased Cloud->`172.31.0.x` (preserving each /30,/31,/29,host-bit shape so the shape/host-bit + rejections still die for the RIGHT reason, inside the new container). `10.12.8.x` rack fixtures unchanged + (metal-admin is unchanged). **96/96 PASS.** +- **`docs/design-decisions.md`:** D-124 AMENDMENT recording the carve + ratified addressing. + +## Still operator-gated (cannot run from the vcloud jumphost) + +The apex is unreachable here + the token is a secret on office1-netbox, so the operator runs, on that host: + +**A. edge-WAN /24s (IPAM hygiene; the edge role/container must exist first):** + # d115-office-carve.py --commit (seeds Office + Edge roles/containers), THEN: + NETBOX_URL=http://10.10.1.10:8000 NETBOX_TOKEN="$(sudo cat /root/netbox-secrets/api.token)" \ + python3 netbox/dc-edge-wan-import.py # dry-run; add --commit (loads 172.30.2.0/24, .3.0/24) + +**B. D-124 transit + rack IP (unblocks `tofu plan`):** + # 1. seed the `transit` role in office1-netbox + # 2. carve 172.31.0.0/24 (a container prefix, role transit) -- confirm free in the LIVE apex + NETBOX_URL=http://10.10.1.10:8000 NETBOX_TOKEN="$(sudo cat /root/netbox-secrets/api.token)" \ + python3 netbox/dc-rack-mgmt-import.py --transit-cidr 172.31.0.0/30 --rack-ip 10.12.8.2 # dry-run; add --commit + +**C. fill the four rack tfvars** (a gitignored local `opentofu/*.auto.tfvars` on the host that runs +`tofu plan`), matching the committed NetBox values: + vr1_dc0_rack_metal_admin_ip = "10.12.8.2" + vr1_dc0_rack_transit_ip = "172.31.0.2" + vr1_dc0_rack_transit_prefix = 30 + vr1_dc0_rack_transit_peer_ip = "172.31.0.1" + +Only C unblocks `tofu plan` (D-125 needs no NetBox object). The importers' die-if-absent preconditions +(role/container/site) are the safety net; run them in the order above. + +## Verification + +- `bash tests/dc-rack-mgmt-import/run-tests.sh` -> 96/96 PASS. +- `bash scripts/repo-lint.sh` -> 0 fail (1 legacy WARN). + +## Revert + +- `git checkout netbox/dc-rack-mgmt-import.py tests/dc-rack-mgmt-import/` (restores `CONTAINER` = Cloud + + the old fixtures); delete the D-124 AMENDMENT (transit supernet) from `docs/design-decisions.md`. No + cloud/apex state touched (nothing was committed to the apex). diff --git a/docs/design-decisions.md b/docs/design-decisions.md index da31fbc..b9e87b1 100644 --- a/docs/design-decisions.md +++ b/docs/design-decisions.md @@ -3690,6 +3690,26 @@ office1<->dc0 mesh transit /30 + rack metal-admin IP) is unchanged; the containment VM still straddles metal-admin (node-facing) + the transit (region-facing), now ALSO hosting the nodes. +### D-124 -- AMENDMENT (2026-07-16): transit supernet CARVED + addressing operator-ratified + +The three scoping flags the `dc-rack-mgmt-import.py` importer surfaced are now RULED (operator, 2026-07-16): +- **(1) `transit` role:** seed a NEW dedicated `transit` role in office1-netbox (the existing `infra` role + is explicitly SHARED infra, not reused) -- mirrors how D-115 seeded `edge`. +- **(2) Transit supernet: CARVE a dedicated one, NOT under Cloud.** Operator-ratified **`172.31.0.0/24`** + (a dedicated management/transit supernet, mirroring how the Edge role got its own `172.30.0.0/16`; a /24 + holds 64 /30s -- ample for all DC<->region + DC<->DC ptp links). The importer's `CONTAINER` is pinned to + this. **Caveat:** the block was picked against a STALE local draft; the operator carves it + the importer + fail-loud preflight is the collision backstop against the LIVE apex. +- **(3) Transit prefix scope:** `dcim.site:vr1-dc0` ("mirror the edge" -- site-scoped like the D-115 DC-edge + /24s). + +**Ratified addressing (confirm free against the live apex before `--commit`):** transit `/30` = +`172.31.0.0/30` (region/peer `.1`, rack `.2`); rack metal-admin IP = `10.12.8.2` (first static in the D-120 +`.2-.49` band). These fill the four `vr1_dc0_rack_*` tfvars: `rack_transit_ip=172.31.0.2`, +`rack_transit_prefix=30`, `rack_transit_peer_ip=172.31.0.1`, `rack_metal_admin_ip=10.12.8.2`. The NetBox +`--commit` runs ON office1-netbox (apex token local there; unreachable from the vcloud jumphost). See +`docs/changelog-20260716-d124-addressing-pin.md`. + ## D-125: VR1 Model B per-DC ISP egress -- bridge-in single-NAT (resolves OBS-3's design gap; egress efficacy is a deploy-time gate) **Status:** ADOPTED (operator ruling 2026-07-16 -- "DC0 and DC1 are supposed to have ISP connections", diff --git a/docs/session-ledger.md b/docs/session-ledger.md index 70d3c5c..064544c 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -284,6 +284,15 @@ immutable CHANGELOGS, decision-record supersession context, or deliberate tombstones (`opentofu/templates/README.md`) -- NOT live drift. Deeper prose in the three reconciled docs + the Stage-4/5 sections still lag Model B (flagged in-banner as historical), not yet rewritten. + +- **D-125 addressing CORRECTED (`e270325`) + tightened (`922dd29`)**, then **D-124 addressing PINNED** + (operator-ratified 2026-07-16): the vcloud ISP /24 is the ruled `172.30.2.0/24` (not a HELD tfvar); + the D-124 transit supernet is CARVED as **`172.31.0.0/24`** (dedicated `transit` role, scope vr1-dc0), + transit /30 `172.31.0.0/30`, rack IP `10.12.8.2`. `dc-rack-mgmt-import.py` `CONTAINER` pinned to + `172.31.0.0/24`; harness rebased, 96/96. **NetBox `--commit` NOT DONE -- office1-netbox (10.10.1.10) is + UNREACHABLE from the vcloud jumphost (measured) + the apex token is a secret on that host; the write is + an on-host operator step** (edge-WAN import, then rack-mgmt import, then fill the 4 `vr1_dc0_rack_*` + tfvars -> `tofu plan`). Exact commands: `docs/changelog-20260716-d124-addressing-pin.md`. NO apex mutation. - **Phase D NOT STARTED** -- SEC-010 rack `ip_forward=0` + firewall artifact + mechanical pre-apply gate. - **Phase E NOT STARTED** -- operator assigns rack transit/30 + rack IP in office1-netbox (apex token), fill tfvars -> `tofu plan` renders. diff --git a/netbox/dc-rack-mgmt-import.py b/netbox/dc-rack-mgmt-import.py index b0d41d1..9011ba3 100644 --- a/netbox/dc-rack-mgmt-import.py +++ b/netbox/dc-rack-mgmt-import.py @@ -38,23 +38,27 @@ NOT reused here because it is explicitly SHARED infrastructure, not the dedicated per-purpose role D-124 asks for. Operator to seed/confirm the `transit` slug in office1-netbox (as d115-office-carve.py seeded `edge`). A missing role dies here. - * CONTAINER = "10.12.0.0/16" (Cloud): the transit CIDR must be subnet_of an EXISTING - container. The apex's only IPv4 container the transit plausibly nests under is Cloud - 10.12.0.0/16 (ipam/aggregates is empty; no dedicated management/transit supernet - exists yet). This gates ACCEPTANCE only -- NetBox auto-nests by CIDR, we set no - parent. If the operator numbers the transit outside Cloud, the tool DIES rather - than misplacing it. Update CONTAINER if a dedicated transit supernet is carved. - * transit prefix SCOPE = dcim.site:vr1-dc0. How an office1<->dc0 link "should" scope - (region vs. site) is unruled; site-scoping to vr1-dc0 mirrors the D-115 DC-edge - /24s (172.30.2.0/24 -> vr1-dc0). FLAGGED -- operator may prefer a region scope. + * CONTAINER = "172.31.0.0/24" (D-124 dedicated transit supernet, operator-pinned + 2026-07-16): the operator ruled a DEDICATED management/transit supernet (NOT under + Cloud 10.12.0.0/16), mirroring how the D-115 Edge role got its own 172.30.0.0/16. + Sized /24 (holds 64 /30s -- ample for all DC<->region + DC<->DC ptp links). The + transit CIDR must be subnet_of this. This container is a PRECONDITION: the operator + carves 172.31.0.0/24 (a container prefix with the `transit` role) in office1-netbox + BEFORE this runs; a missing/collision container makes the tool DIE (fail-loud is the + backstop for the candidate block being picked against a stale draft). This gates + ACCEPTANCE only -- NetBox auto-nests by CIDR, we set no parent. + * transit prefix SCOPE = dcim.site:vr1-dc0 (RULED 2026-07-16: "mirror the edge" -- + site-scoped like the D-115 DC-edge /24s, 172.30.2.0/24 -> vr1-dc0). * The rack IP band (metal-admin 10.12.8.0/22, static .2-.49, gw .1) is enforced ARITHMETICALLY (D-120/D-124), NOT via a prefix-object lookup: metal-admin is not registered as a /22 prefix in the apex draft, and the band is a convention, not a container. This asymmetry with the transit check is intentional. -Usage (on office1-netbox / through a tunnel, with the sandbox token): +Usage (on office1-netbox / through a tunnel, with the sandbox token). D-124 values +operator-ratified 2026-07-16 (transit supernet 172.31.0.0/24, transit /30 +172.31.0.0/30, rack IP 10.12.8.2) -- confirm free against the LIVE apex first: NETBOX_URL=http://10.10.1.10:8000 NETBOX_TOKEN= \ - python3 netbox/dc-rack-mgmt-import.py --transit-cidr --rack-ip <10.12.8.X> + python3 netbox/dc-rack-mgmt-import.py --transit-cidr 172.31.0.0/30 --rack-ip 10.12.8.2 ... same, add --commit, to write. """ import argparse @@ -73,7 +77,7 @@ SANDBOX_HOSTS = {"localhost", "127.0.0.1", "10.10.1.10"} ROLE_SLUG = "transit" # the transit's OWN role (D-124); a precondition here -CONTAINER = "10.12.0.0/16" # Cloud; the transit CIDR must be subnet_of this (a precondition) +CONTAINER = "172.31.0.0/24" # D-124 dedicated transit supernet (operator-pinned 2026-07-16); transit /30 must be subnet_of this (precondition) SITE_SLUG = "vr1-dc0" # the transit prefix is site-scoped here (mirrors the D-115 edge /24s) STATUS = "active" # a real segment on the wire diff --git a/tests/dc-rack-mgmt-import/run-tests.sh b/tests/dc-rack-mgmt-import/run-tests.sh index d8ca624..e91e828 100644 --- a/tests/dc-rack-mgmt-import/run-tests.sh +++ b/tests/dc-rack-mgmt-import/run-tests.sh @@ -26,13 +26,13 @@ python3 "$S" --help >/dev/null 2>&1 && ok || bad "--help -> 0" # no env -> must fail loud, not guess a target -NETBOX_URL= NETBOX_TOKEN= python3 "$S" --transit-cidr 10.12.60.0/30 --rack-ip 10.12.8.5 \ +NETBOX_URL= NETBOX_TOKEN= python3 "$S" --transit-cidr 172.31.0.0/30 --rack-ip 10.12.8.5 \ >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "missing NETBOX_URL/TOKEN must fail" # NO INVENTED LITERAL -- both inputs are REQUIRED (no default CIDR/IP baked in) NETBOX_URL=http://10.10.1.10:8000 NETBOX_TOKEN=x python3 "$S" --rack-ip 10.12.8.5 \ >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "missing --transit-cidr must fail (no invented CIDR)" -NETBOX_URL=http://10.10.1.10:8000 NETBOX_TOKEN=x python3 "$S" --transit-cidr 10.12.60.0/30 \ +NETBOX_URL=http://10.10.1.10:8000 NETBOX_TOKEN=x python3 "$S" --transit-cidr 172.31.0.0/30 \ >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "missing --rack-ip must fail (no invented IP)" grep -q 'default=os.environ.get("TRANSIT_CIDR")' "$S" && ok || bad "lost the TRANSIT_CIDR env fallback" grep -q 'default=os.environ.get("RACK_IP")' "$S" && ok || bad "lost the RACK_IP env fallback" @@ -47,7 +47,7 @@ grep -q 'yes-write-upstream' "$S" && ok || bad "lost the --yes-write-upstream gate" grep -q 'SANDBOX_HOSTS' "$S" && ok || bad "lost the SANDBOX_HOSTS gate" out="$(NETBOX_URL=https://netbox.baldurkeep.com NETBOX_TOKEN=x python3 "$S" \ - --transit-cidr 10.12.60.0/30 --rack-ip 10.12.8.5 --commit 2>&1)" + --transit-cidr 172.31.0.0/30 --rack-ip 10.12.8.5 --commit 2>&1)" printf '%s' "$out" | grep -q "REFUSING to --commit" && ok \ || bad "did NOT refuse a --commit to a non-sandbox host (it would have written to production)" @@ -66,7 +66,7 @@ # THE D-124 SCHEME VALUES -- role, container, site scope, metal-admin band, rack dns. grep -qF 'ROLE_SLUG = "transit"' "$S" && ok || bad "transit role slug changed" -grep -qF 'CONTAINER = "10.12.0.0/16"' "$S" && ok || bad "container is not 10.12.0.0/16 (Cloud)" +grep -qF 'CONTAINER = "172.31.0.0/24"' "$S" && ok || bad "container is not 172.31.0.0/24 (D-124 dedicated transit supernet, operator-pinned)" grep -qF 'SITE_SLUG = "vr1-dc0"' "$S" && ok || bad "transit site scope is not vr1-dc0" grep -qF 'METAL_ADMIN = "10.12.8.0/22"' "$S" && ok || bad "metal-admin is not 10.12.8.0/22" grep -qF 'RACK_DNS = "vvr1-dc0"' "$S" && ok || bad "rack dns name is not vvr1-dc0" diff --git a/tests/dc-rack-mgmt-import/test_logic.py b/tests/dc-rack-mgmt-import/test_logic.py index 8ec679e..63f0677 100644 --- a/tests/dc-rack-mgmt-import/test_logic.py +++ b/tests/dc-rack-mgmt-import/test_logic.py @@ -13,7 +13,7 @@ `role` is a choice field) -- identity is in dns_name/description. * BAND arithmetic: rack IP within metal-admin 10.12.8.0/22, D-120 static band .2-.49, NOT the .1 gateway, NOT .0. - * TRANSIT shape: /30 or /31, host-bits-clear, subnet_of the Cloud container. + * TRANSIT shape: /30 or /31, host-bits-clear, subnet_of the transit container. * ALREADY-PRESENT = SKIP (idempotent), distinct from missing-infra = die. * DRY BY DEFAULT: no --commit writes NOTHING. * UPSTREAM-WRITE guard: --commit to a non-sandbox host refuses without the flag. @@ -76,9 +76,9 @@ # The operator-supplied, NetBox-assigned example values under test. Chosen INSIDE the -# scheme: transit /30 subnet_of Cloud 10.12.0.0/16; rack IP in the .2-.49 static band. -TRANSIT = "10.12.60.0/30" -TRANSIT31 = "10.12.60.0/31" +# scheme: transit /30 subnet_of the dedicated transit supernet 172.31.0.0/24; rack IP in the .2-.49 static band. +TRANSIT = "172.31.0.0/30" +TRANSIT31 = "172.31.0.0/31" RACK = "10.12.8.5" RACK_ADDR = "10.12.8.5/22" # how the tool stores it (host + metal-admin /22 mask) @@ -104,9 +104,9 @@ ok(label) -# Full-precondition fixture builders (transit role + Cloud container + vr1-dc0 site). +# Full-precondition fixture builders (transit role + transit container + vr1-dc0 site). ROLE = {"slug": "transit", "name": "Transit", "id": 7} -CONTAINER = {"prefix": "10.12.0.0/16", "id": 10} +CONTAINER = {"prefix": "172.31.0.0/24", "id": 10} SITE_DC0 = {"slug": "vr1-dc0", "name": "VR1 DC0", "id": 20} @@ -225,7 +225,7 @@ check(len(fk.creates) == 0, "missing-role run wrote nothing") fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[], sites=[SITE_DC0]) -run_dies("missing Cloud container 10.12.0.0/16 is REJECTED", base_args(["--commit"]), fk) +run_dies("missing dedicated transit supernet 172.31.0.0/24 is REJECTED", base_args(["--commit"]), fk) check(len(fk.creates) == 0, "missing-container run wrote nothing") fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[CONTAINER], sites=[]) @@ -235,20 +235,20 @@ # ----------------------------------------------------------------------------- # 8. Transit CIDR shape / placement rejects. # ----------------------------------------------------------------------------- -run_dies("a transit OUTSIDE the Cloud container is REJECTED", +run_dies("a transit OUTSIDE the transit container is REJECTED", ["--transit-cidr", "192.168.0.0/30", "--rack-ip", RACK, "--commit"], full_fake()) run_dies("a /29 transit is REJECTED (not point-to-point)", - ["--transit-cidr", "10.12.60.0/29", "--rack-ip", RACK, "--commit"], full_fake()) + ["--transit-cidr", "172.31.0.0/29", "--rack-ip", RACK, "--commit"], full_fake()) run_dies("a /32 transit is REJECTED", - ["--transit-cidr", "10.12.60.1/32", "--rack-ip", RACK, "--commit"], full_fake()) -run_dies("a host-bits-set transit (10.12.60.1/30) is REJECTED", - ["--transit-cidr", "10.12.60.1/30", "--rack-ip", RACK, "--commit"], full_fake()) + ["--transit-cidr", "172.31.0.1/32", "--rack-ip", RACK, "--commit"], full_fake()) +run_dies("a host-bits-set transit (172.31.0.1/30) is REJECTED", + ["--transit-cidr", "172.31.0.1/30", "--rack-ip", RACK, "--commit"], full_fake()) run_dies("a non-CIDR transit is REJECTED", ["--transit-cidr", "not-a-cidr", "--rack-ip", RACK, "--commit"], full_fake()) # each of the above must have written nothing -- prove it once with a fresh fake _fk = full_fake() run_dies("bad transit writes nothing (proof)", - ["--transit-cidr", "10.12.60.0/29", "--rack-ip", RACK, "--commit"], _fk) + ["--transit-cidr", "172.31.0.0/29", "--rack-ip", RACK, "--commit"], _fk) check(len(_fk.creates) == 0, "the rejected bad-transit run wrote NOTHING") # ----------------------------------------------------------------------------- @@ -324,7 +324,7 @@ # 13. Structural pins (a changed constant a behavioral test alone would miss). # ----------------------------------------------------------------------------- check(T.ROLE_SLUG == "transit", "ROLE_SLUG is 'transit'") -check(T.CONTAINER == "10.12.0.0/16", "CONTAINER is 10.12.0.0/16 (Cloud)") +check(T.CONTAINER == "172.31.0.0/24", "CONTAINER is 172.31.0.0/24 (dedicated transit supernet)") check(T.SITE_SLUG == "vr1-dc0", "SITE_SLUG is vr1-dc0 (transit site scope)") check(T.METAL_ADMIN == "10.12.8.0/22", "METAL_ADMIN is 10.12.8.0/22") check(T.STATIC_BAND_LOW == 2 and T.STATIC_BAND_HIGH == 49,