# 2026-07-14 -- D-119: region-qualify the VR1 DC namespace (Stage 2 close-out C3 / gap #19)

Closes the collision that BLOCKED Stage 3. **D-119 COMPLETES D-117; it does not reverse it.**

## The bug, in one line

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 ruling (operator, 2026-07-14)

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 prize: the off-by-one becomes structurally impossible

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

## TWO REAL BUGS found by the review sweep, which the naming fix alone would NOT have closed

**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.

## What changed

- `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**.
- All 6 VR1 runbooks + `netbox/README.md` + `docs/vr1-office1-as-built.md` call sites.
- **D-114 amendment:** the DC containment VMs `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

`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.

## The `tofu apply` is NOT done -- it is GATED

`opentofu/` 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").

## Revert

    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.
