Closes the collision that BLOCKED Stage 3. D-119 COMPLETES D-117; it does not reverse it.
The token dc0 meant two different clouds depending on which file you were reading: VR0's LIVE testcloud in scripts/lib-net.sh, and VR1's FIRST DC in the NetBox importer. One string, two clouds, one of them in production.
The repo adopts the apex's names verbatim, region-qualified:
vr0-dc0 VR0's DC0 the LIVE testcloud (a DIFFERENT region) vr1-dc0 VR1's FIRST DC GUA 2602:f3e2:f02::/48 vr1-dc1 VR1's SECOND DC GUA 2602:f3e2:f03::/48
Bare dc0/dc1/dc2 are now REJECTED LOUDLY everywhere. Accepting them "for compatibility" would preserve the exact ambiguity being deleted.
NO PRODUCTION IPAM WRITE. The apex was already correct and self-consistent; the REPO was the only surface out of step. MEASURED against netbox.baldurkeep.com before any edit:
2602:f3e2:f02::/48 -> site vr1-dc0 "Virtual Region 1 (VR1) Datacenter 0 (DC0)" 2602:f3e2:f03::/48 -> site vr1-dc1 "Virtual Region 1 (VR1) Datacenter 1 (DC1)"
The alternative (rename the apex to match the repo's dc1/dc2) was considered and REJECTED: it needed a production write on the IPAM authority, made VR1 the only 1-indexed region, and would have created a worse ambiguity (vr1-dc1 = VR1's FIRST DC vs vr0-dc1 = VR0's SECOND). See D-119.
The importer's DC->site map is now an IDENTITY (vr1-dc0 -> slug vr1-dc0). There is no offset table left to get wrong -- and the original defect was precisely a WRONG LOOKUP TABLE. An assert enforces it:
assert all(k == v["slug"] for k, v in SITES.items()) # D-119
BREAK-1 -- corrupted descriptions. The importer built labels with f"VR1 {dc.upper()} ...". Fine when dc was "dc0"; under D-119 dc is "vr1-dc0", rendering "VR1 VR1-DC0 provider-public" on all 36 prefixes. Same defect class as the original bug -- DERIVING a label by munging a token instead of looking it up -- hiding in the description field, where slug-focused review missed it. Now f"{site_cfg['name']} ...", looked up.
BREAK-2 -- DC_GUA_PREFIX was never cross-checked against --dc. THIS IS THE IMPORTANT ONE. --dc vr1-dc0 DC_GUA_PREFIX=<f03::/48> was accepted -- it writes the SECOND DC's GUA prefixes, carved with the FIRST DC's ULA nibble (DC_V6_INDEX is keyed off --dc, independently), all scoped to the FIRST DC's site. A silently mis-bound datacenter, assembled from two disagreeing sources. Identity-mapping the site slug makes the slug unfalsifiable but leaves the addressing free-floating -- so the naming fix alone would NOT have finished the job. Now guarded by EXPECTED_GUA, which pins the apex's measured binding and refuses a mismatched pair.
scripts/lib-net.sh, lib-hosts.sh -- region-qualified selectors; bare dcN REJECTED. vr0-dc0 and vr1-dc0 are SEPARATE case arms on purpose: they are behaviourally identical today but semantically independent (vr1-dc0 no-ops because D-101 says it INHERITS VR0 DC0's v4 layout). The day D-101 stops holding, one diverges and the other must not.netbox/dc-dc-prefixes-import.py -- identity SITES, the EXPECTED_GUA guard, the description fix, DC1_V4_SUPERNET/DC2_V4_SUPERNET -> VR1_DC1_V4_SUPERNET (BOTH old names rejected BY NAME: an unqualified "DC1" could mean VR0's dc0 OR VR1's second DC).scripts/dc-dc-{dr-drill,radosgw-multisite,rbd-mirror,ceph-disk-budget}.sh, {phase-00-maas-standup,carve-host-interfaces,reenroll-hosts}.sh -- selectors + DC= hints. rbd-mirror's --site-name aligned too: it is a DIFFERENT namespace that LOOKS identical, and --dc vr1-dc0 --site-name dc1 on one command line is a re-created collision.opentofu/ -- dc1_* -> vr1_dc0_*, dc2_* -> vr1_dc1_*, with moved {} blocks.netbox/README.md + docs/vr1-office1-as-built.md call sites.vdc1/vdc2 -> vvr1-dc0/vvr1-dc1. Neither is built, so this is free -- and D-114's own DR primitive is virsh destroy vdc1, which under the old naming reads as "destroy VR1 DC1" while MEANING "destroy VR1 DC0". A mislabelled destroy command in a DR drill is not cosmetic. voffice1 KEEPS its name.tests/dc-selector 21 -> 30 (new: every bare dcN must be REJECTED). tests/dc-dc-prefixes-import 40 -> 82 (new: the identity invariant, EXPECTED_GUA in BOTH mismatch directions, that a mismatched run writes NOTHING, that the MATCHING pair still succeeds, and that no description contains the munged VR1 VR1-DC0). GAUNTLET ALL GREEN (57 harnesses); repo-lint 0 fail.
The old harnesses were not merely stale -- they pinned the wrong mapping, and would have gone green while enforcing the bug. That is the failure mode this change exists to kill.
tofu apply is NOT done -- it is GATEDopentofu/ changes rename the libvirt objects (dc_name/link_name interpolate straight into each object's name, which is ForceNew), so the plan REPLACES them. moved {} cannot suppress a ForceNew -- it is there so the plan reads as 11 clean "must be replaced" lines instead of 11 unrelated destroy+create pairs, i.e. so it is REVIEWABLE.
Plan: 11 to add, 0 to change, 11 to destroy.
Safe, MEASURED before the rename: all 11 objects are EMPTY -- no guests attached (Stage 3 has not run, so no DC node VMs exist) and no volumes in either DC pool (the only volumes in state belong to office1_opnsense, ubuntu_noble_base, voffice1, none of which sit in a DC pool). Office1's live networks do not appear in the plan at all.
Not applied. A destroying apply is operator-gated. Re-run tofu plan at execution and STOP if anything shows as ATTACHED (standing lesson 2: "updated in-place" does NOT mean "no restart").
git revert <this commit>
# opentofu: the moved{} blocks make the reverse direction symmetrical; re-plan before applying.
# No NetBox state to revert -- this change made ZERO writes to any NetBox.