# DC-DC virtual regional two-DC buildout design (VR1)

**Status:** DESIGN / PLAN. Originating design for the virtual regional two-DC test
environment (VR1: DC1, DC2, Office1 headend). PROPOSED decisions live in
`docs/design-decisions.md` as D-100..D-110. No live mutation is authorized from this
document; it is the plan the phased runbooks execute against.

**Governing constraint:** minimize delta to Roosevelt (the future bare-metal multi-DC
production site). Every virtual choice maps to a defensible real one; the one deliberate
exception is tagged in the shim register (Section 9).

**Ground truth / copy point:** seeded from `openstack-caracal-ipv4` at commit 8813efc into
`openstack-caracal-dc-dc` (verified). Six-plane values are `scripts/lib-net.sh`
(CIDR-keyed); fabrics / VLANs are `docs/maas-as-built-reference.md`; NetBox (vcloud-local)
is the IPAM apex for all address literals.

---

## 1. Settled inputs (locked with the operator)

- Topology: full dark-fiber triangle (DC1<->DC2, DC1<->Office1, DC2<->Office1) plus a
  per-site OPNsense simulated ISP edge. Mesh, not star. (D-100)
- Independent clouds: DC1 and DC2 are separate Charmed OpenStack clouds, separate
  Keystones. No control-plane federation. Only the replication plane crosses fiber. (D-100)
- IaC seam: OpenTofu creates the libvirt "physical" layer (domains, virtual networks,
  OPNsense, Office1 service VMs) from Office1; MAAS enlists / commissions / deploys the
  node VMs; Juju deploys the bundle. NetBox is IPAM apex. (D-103)
- Controllers: one single-unit Juju controller per DC, in-DC, with backups; HA deferred to
  Roosevelt. Office1 hosts no OpenStack controller. (D-104)
- CAPI mgmt per DC, in-cloud single-homed; COS and Magnum / CAPI in both DCs. (D-105)
- DNS: Designate reactivated in-bundle per DC (backend designate-bind), per-DC zones,
  A + AAAA. Supersedes D-019, reactivates D-008. (D-106)
- Airgap at the node level; per-DC artifact mirror; NTP from each DC's edge (edge syncs
  internet); Office1 out of the core-service path. (D-107)
- Replication: Tier A, bidirectional -- cinder-backup via radosgw multisite; Glance via
  rbd-mirror; Nova ephemeral not replicated; one-way then two-way; carrier is the
  IPv6-only ULA replication plane. (D-108, corrects the DR-seed v4 carrier)
- Vault: per-DC independent roots, pinned 1.8/stable; v4 + IPv6-SAN certs. (D-109)
- IPv6: family follows reachability -- v6 wherever possible, v4 only where forced. (D-101)
- Numbering: DC-DC series starts D-100; 076-099 reserved source-band. (D-110)

## 2. Address families (per DC) -- summary of D-101

| Plane | v1/DC0 v4 (DC1 inherits) | VR1 family | Note |
|---|---|---|---|
| provider-public | 10.12.4.0/22 | v4 + GUA | public API VIPs + FIP/ext_net; GUA via owned edge |
| metal-admin | 10.12.8.0/22 | v4 only | operator / MAAS / PXE / admin API; DC-local |
| metal-internal | 10.12.12.0/22 (VID 103) | v4 + ULA | API endpoints dual-stack; MySQL/RabbitMQ stay v4 |
| data-tenant | 10.12.16.0/22 | ULA only | geneve overlay; tenant GUA delegated separately |
| storage | 10.12.32.0/22 | ULA only | Ceph public; ms_bind_ipv6 |
| replication | 10.12.36.0/22 | ULA only | Ceph cluster; cross-DC leg rides this (ULA) |

DC2 mirrors this structure on a distinct, non-overlapping supernet. All literals
(ULA /48, per-DC GUA carve from the /36, DC2 v4 supernet) are NetBox-assigned -- not
hardcoded. PXE and datastore east-west are the two deliberate v4-retained points.

## 3. Host and resource plan (Phase-0 gates)

Single vcloud host: ~256 vCPU / 1 TiB RAM / 10 TiB disk, dedicated to VR1. CPU and RAM
are abundant; disk is the binding constraint (two Ceph clusters + per-DC backup/mirror +
images). Phase-0 gates:

- Nested KVM: confirm / enable (`nested` module param; Intel vs AMD differs). Verify guest
  virtualization is exposed to the DC node VMs.
- Ceph replication factor: **RULED 2026-07-09 (operator):** target size=3 / min_size=2
  by default (production-honest, lowest delta), CONFIRMED. This is CONDITIONAL on the disk
  budget. The Phase-0 disk math must account for: DC1 Ceph (size=3) + DC2 Ceph (size=3) +
  per-DC radosgw backup pools (themselves replicated within each cluster, and duplicated
  cross-DC by multisite) + Glance rbd-mirror targets + images + per-DC mirrors. If size=3
  across both clusters plus the backup/mirror footprint exceeds 10 TiB, fall back to size=2
  as an EXPLICIT, documented test-only deviation (its own decision note) -- CONFIRMED as the
  ONLY path to size=2: never a silent choice, Roosevelt sizing must not inherit a hidden 2x.
  Thin-provisioning is the lever; the reserve split (Ceph vs backup/mirror/images) is set in
  Phase 0 from the measured DC0 per-node OSD footprint.
- MTU: measure the host L2 MTU. Compute the geneve-over-v6 budget (D-101, folded in from
  D-102 per the 2026-07-09 operator ruling). Prefer jumbo (9000) end-to-end; if pinned at
  1500, set the reduced tenant MTU consistently. Do not assume jumbo. MTU stacking (nested
  virt + geneve + VLAN) is the classic failure mode.

## 4. Phase spine

Each phase has a single goal, a verify-before-mutate gate, and owning D-entries. Phases run
in order; a phase is not entered until the prior gate passes. Per operating discipline, the
per-phase runbooks (later deliverables) carry the command-level steps with harnesses; this
document is the plan they execute.

### Phase 0 -- vcloud host preparation
Goal: turn the bare vcloud host into a substrate the Office1 headend can deploy from.
Build: nested KVM enabled; libvirt storage pools + thin-provisioning; the dark-fiber and
per-site ISP-edge virtual networks; the six per-DC plane segments (as virtual networks);
the MTU plan set on the underlay.
Gate: nested KVM verified; disk budget computed and the Ceph size decision recorded;
measured MTU recorded; virtual networks present and isolated as designed.
Owns: D-100 (fabric), D-101 (MTU -- folds in former D-102 per the 2026-07-09 operator ruling), Section 3 gates.

### Phase 1 -- Office1 headend standup
Goal: stand up the operator's deployment headend first, before any DC.
Build: MAAS region controller; OpenTofu; NetBox (vcloud-local, importer extended for
multi-DC + dual-stack); GitBucket (vcloud-local mirror); Tailscale front door (Office1 only).
Gate: MAAS region reachable; NetBox authoritative and populated (planes, per-DC v4, ULA/GUA
carve); GitBucket serving; OpenTofu can reach the vcloud host libvirt; Tailscale access
confirmed to Office1 only.
Owns: D-103 (Office1 service VMs are OpenTofu-created), D-107 (headend is not a core-service
provider).

### Phase 2 -- OpenTofu builds each DC substrate (from Office1)
Goal: from Office1, script each DC's "physical" layer into existence.
Build: per DC, OpenTofu creates the node-VM libvirt domains (SHIM -- Section 9), the DC's
plane networks, and the DC's OPNsense edge; registers the DC libvirt host to that DC's MAAS
rack controller as a virsh VM-host.
Gate: MAAS rack controller per DC up; MAAS discovers the OpenTofu-created node VMs; edge
routing + simulated ISP uplink up; netem parameters applied and measured.
Owns: D-103 (seam), D-100 (edges).

### Phase 3 -- MAAS enlist / commission / deploy (per DC)
Goal: bring the discovered node VMs to a deployed OS, ready for Juju.
Build: per DC, MAAS commissions and deploys the node VMs; provider NIC left RAW (br-ex
discipline, D-100); the VLAN-103 metal-internal bridge stack pre-built by MAAS.
Gate: nodes deployed; the six planes present per node with correct fabrics/VLANs; provider
NIC raw; PXE (v4) working; per-DC artifact source reachable. (DOCFIX-204, 2026-07-27:
this line formerly read "per-DC mirror reachable from nodes; NTP from edge working".
The node-side half is gate G17 -- powered-off nodes cannot be probed in Stage 4 under
the READY-handoff ruling -- and the edge-NTP clause is SUPERSEDED by D-129(iv), which
keeps the MAAS hierarchy as time authority. docs/CURRENT-STATE.md is the status
authority for both.)
Owns: D-103, D-100 (br-ex), D-107 (mirror + NTP).

### Phase 4 -- Juju controller + OpenStack bundle (per DC)
Goal: a running independent OpenStack cloud per DC.
Build: per DC, bootstrap the single-unit Juju controller in-DC; deploy the Caracal bundle;
Vault init with per-DC independent root, issuing v4 + IPv6-SAN certs; the family matrix
applied (ULA on data/storage/replication, dual-stack metal-internal API, GUA provider).
Gate: `preflight.sh` PASS before add-model; post-deploy `cloud-assert.sh --capture` per DC;
controller backup taken and stored; Ceph-over-v6 and geneve-over-v6 verified.
Owns: D-104 (controllers), D-109 (Vault), D-101 (families).

### Phase 5 -- DR wiring and failover drill
Goal: prove Tier-A recoverability, one-way first, then two-way.
Build: replication-plane peering (ULA); radosgw multisite zonegroup (two DCs as zones);
rbd-mirror for the Glance pool; cinder-backup to the replicated object store; consistency
groups where needed. Run the failover / failback drill (Section 8). Stage one-way, then
enable two-way after a clean drill.
Gate: one-way drill clean (RTO/RPO measured); then two-way; controller restore drill
(D-104) exercised; RTO recorded as a measured output, not an SLA.
Owns: D-108 (replication), D-104 (control-plane backup/restore in the drill).

### Phase 6 -- Designate, COS, Magnum / CAPI (per DC)
Goal: complete the service set in both DCs.
Build: per DC, Designate (designate-bind) with per-DC zones + A/AAAA per the D-008
bootstrap order; COS observability; Magnum / CAPI with in-cloud single-homed capi-mgmt
(`domain-setup` re-run per D-046).
Gate: Designate resolving A + AAAA; FQDN-SAN certs valid (D-019/D-021 root cause closed);
COS scraping; a CAPI workload cluster comes up per DC.
Owns: D-106 (Designate), D-105 (CAPI/COS).

## 5. Cross-cutting: OpenTofu / MAAS / NetBox / Juju boundary

- NetBox: IPAM apex. All address values originate here; MAAS and overlays consume them.
- OpenTofu: create/destroy of libvirt domains, virtual networks, OPNsense, Office1 VMs;
  runs from Office1. Owns the "physical" layer only.
- MAAS: enlist (discover) / commission / deploy / power / release of node VMs. Does not
  compose. One region (Office1) + one rack per DC (virsh VM-host).
- Juju: deploys and manages the OpenStack bundle onto MAAS machines; one controller per DC.

## 6. WAN simulation

`tc netem` on the inter-DC and site-uplink virtual segments. Default lean: same-metro dark
fiber (low single-digit ms, jumbo-capable). Parameters (latency / jitter / loss / rate) are
a Phase-0 knob and are recorded per drill so RTO/RPO actuals are attributable to a known
link profile. Re-tune to the confirmed Roosevelt inter-DC profile when known.

## 7. IPv6 rehearsal scope

VR1 rehearses the full developed IPv6 plan because the simulated OPNsense edge is ours (the
D-015 upstream-router gate does not apply): internal IPv6-only ULA (data / storage /
replication / Octavia lb-mgmt / P2P), internal dual-stack ULA on metal-internal API
endpoints, and external GUA (provider public VIPs, native v6 ext_net, tenant delegation).
Ceph-over-v6 (`ms_bind_ipv6`), geneve-over-v6 MTU, and native v6 tenant routing are the
high-value Roosevelt proofs. Phase-A internal-three goes straight to v6-only (lean;
dual-then-flip is the conservative fallback if Phase-0 verification is not clean).

## 8. Failover / failback drill skeleton (from the DR seed, carrier corrected)

Failover (DC1 lost, recover at DC2):
1. Confirm DC1 truly down (avoid split-brain from a transient partition).
2. Glance: promote the mirrored Glance pool at DC2 (force-promote if DC1 unreachable);
   re-register images into DC2 Glance.
3. Cinder: restore required volumes into DC2 Cinder from the replicated backups.
4. Rebuild instances from image + restored volume; re-create Neutron ports / FIPs / SGs.
5. Record RTO / RPO actuals.

Failback (DC1 recovered) -- split-brain-safe for two-way:
1. Do NOT let recovered DC1 resume as primary automatically.
2. Demote DC1's Glance pool; resync from DC2 (now primary).
3. Reconcile Cinder: back up any DC2-side changes; restore into DC1.
4. In a controlled window, optionally flip primary back (demote DC2, promote DC1).
5. Verify both directions healthy; record the drill.

## 9. Shim register -- the no-Roosevelt-analog steps

The only steps with NO Roosevelt analog (tag them clearly; do not treat as reusable
production IaC):

- OpenTofu "create the node VMs" module (D-103). In Roosevelt, node "hardware" already
  exists; that step is physical racking + BMC enlistment. Everything from MAAS enlistment
  onward IS Roosevelt-transferable.
- `tc netem` WAN simulation (D-100). In Roosevelt the inter-DC link is real fiber; netem
  is replaced by the physical link. The route + bandwidth budget it stands in for IS real.
- Single-unit controllers (D-104) and single-unit rbd-mirror daemon (D-108) are VR1 test
  economies; Roosevelt scales both to HA.

## 10. Operator redline -- RULED 2026-07-09 (Stage 0 ratification complete)

All six items below were ruled by the operator on 2026-07-09; see the referenced
D-NNN entries in `docs/design-decisions.md` for the authoritative recorded text.
D-100 through D-110 are now ADOPTED (D-102 MERGED into D-101; D-109 confirmed
standalone) -- Stage 0 of `docs/dc-dc-deployment-workflow.md` is CLEARED.

- Ceph size=3 vs a documented size=2 fallback: **RULED** -- size=3 by default,
  size=2 only as an explicit, logged Phase-0 fallback if the disk math doesn't
  fit (Section 3).
- netem link profile: **RULED** -- same-metro lean now (D-100), re-tune when a
  specific Roosevelt inter-DC target is known.
- Whether D-102 folds into D-101, and whether D-109 stands alone: **RULED** --
  D-102 folds into D-101; D-109 stays standalone (D-101, D-109).
- metal-admin admin-API ULA leg: **RULED** -- ADDED (reverses the stated lean;
  D-101).
- COS per-DC-only vs an Office1 roll-up: **RULED** -- per-DC only, confirming
  the stated lean (D-105).
- Mirror sync topology: **RULED** -- independent upstream pull per DC,
  confirming the stated lean (D-107).

## 11. Landing this deliverable

1. Append the body of `docs/dc-dc-D100-D110.append.md` (from its first `## D-100` line) to
   the end of `docs/design-decisions.md`, then remove the fragment file.
2. Run `bash scripts/ledger-scan.sh`; confirm D next-free = 111 and the machine-derived
   ledger block regenerates cleanly. Do not hand-edit the machine-derived block.
3. Add a one-line entry in the main-chat fence of `docs/session-ledger.md` recording this
   deliverable (design doc + D-100..D-110 PROPOSED).
4. `bash scripts/repo-lint.sh` for ASCII/LF/format (ignore any next-free noise above 099 --
   known band bug; ledger-scan is the next-free authority).
5. Commit. These are PROPOSED: present for redline, do not implement until the operator
   ratifies each to ADOPTED.
