# 2026-07-15 -- Stage 3 batch-deploy prep: OpenTofu wiring, DC-edge NetBox importer, HA overlay, D-123

## Context

Operator directed "get as much ready as possible so we can batch deploy with the least
operator interaction." This batch prepares the Stage-3 substrate + Stage-5 HA artifacts so the
live steps reduce to review-plan-and-apply. NO cloud mutation this session -- all authoring +
offline validation; every `tofu apply`, netbox `--commit`, and juju deploy stays operator-gated.
Fanned out across three disjoint areas (opentofu / netbox / overlays); the opentofu agent hit a
transient API stall and its stream was completed in-session by the main loop.

## What changed

**D-123 (PROPOSED) -- DC site-down DR model / node-VM placement.** `docs/design-decisions.md`.
Surfaced a real tension: D-122 records the operator's "each site is one VM, `virsh destroy` =
site-down" intent, but the ADOPTED D-103/D-114 seam + the built tooling create the OpenStack node
VMs as **vcloud-level** domains that MAAS merely DISCOVERS (containment VM scoped to MAAS/LXD +
non-stack machines only). Model A (recommended): nodes stay vcloud-level; `vvr1-dc0` is a MAAS/LXD
headend VM (the proven `voffice1` pattern via `site-headend-install.sh`, which already names it a
target); site-down = destroy the `vr1-dc0-*` domain GROUP. Model B: true single-VM containment
(nodes nested), 4-level nesting, heaviest. **Needs a one-line operator ruling.**

**OpenTofu Stage-3 wiring (`opentofu/`).**
- **New module `modules/site-wan/`** -- a NAT'd per-site simulated-ISP uplink /24 (generalizes the
  virsh-created `office1-wan`; operator 2b ruling). MTU 1500 (ISP-uplink domain, NOT jumbo). Schema
  CONFIRMED against the installed provider via `tofu providers schema -json` (the method dc-planes
  calls for): `forward = { mode = "nat" }`, `ips = [{ address, prefix }]`, `mtu = { size }`,
  attribute-style like dc-planes. DHCP omitted (the edge WAN is static, like office1-opnsense .2).
  Per-module `versions.tf` (DOCFIX-179).
- **`main.tf` Stage-3 section** (NOT YET APPLIED, gated): `module "vr1_dc0_wan"` (172.30.2.0/24,
  D-115); `module "vr1_dc0_opnsense"` (D-122: 2-NIC, `memory_mib=2048`/`vcpu=2`/nano, no
  `disk_size_bytes`/`config_iso_path`, LAN = provider-public plane, WAN = the uplink); 8 node VMs via
  a single `for_each` over a `locals` map in D-121 Option C sizes (3 control 16/65536/150GiB, 2
  compute 12/49152/100GiB, 3 storage 8/24576/550GiB), six NICs each with **metal-admin first (PXE)**;
  netem HELD as a comment (Step 11, runtime SSH target + unruled params; the vr1-dc0<->office1 mesh
  bridge is `virbr7`, measured). **HELD pending D-123:** the `vvr1-dc0` headend + `maas-vm-host` (Step
  9) blocks are not wired.
- Fixed the stale `vr1_dc1_planes` comment (supernet IS assigned, D-115; out of scope by sequencing).
- **`tofu validate` -> Success**; `scripts/opentofu-validate.sh` -> 11/11 modules PASS (incl. site-wan).

**NetBox DC-edge WAN importer (`netbox/dc-edge-wan-import.py` + `tests/dc-edge-wan-import/`).**
Registers the D-115 DC-edge /24s in office1-netbox: `172.30.2.0/24` -> site `vr1-dc0`,
`172.30.3.0/24` -> site `vr1-dc1`, role `edge`, `scope_type=dcim.site` -- scoping CONFIRMED against
`d115-office-carve.py`/`sandbox-fidelity-check.py` (how office1-wan 172.30.1.0/24 is bound), not the
frozen draft (which predates D-115). Stdlib-only, mirrors `d120-compose-bands.py`: WHOLE-PLAN
PREFLIGHT (both sites resolve before any create, so a missing site cannot half-write), DRY-BY-DEFAULT,
`SANDBOX_HOSTS` guard + `--yes-write-upstream` refusal, WAF-safe UA. Creates ONLY the two /24s (role +
container + sites are read-only preconditions). Harness: **58/58 PASS** (19 static + 39 behavioral,
in-process against a fake NetBox). NOT run against a live NetBox (token operator-held).

**Bundle HA scale-up overlay (`overlays/dc-ha-scaleup.yaml`).** VR1-specific overlay (NOT a bundle.yaml
edit -- VR0 stays single-unit per D-009) implementing D-121: 14 services -> `num_units: 3`, one-per-
control-node placement via a 3-entry `to:` with `{{VR1_DC0_CONTROL_0N}}` tokens (Stage-4-binding
banner). Three correctness items beyond the literal bump, each flagged in-file: (A) rabbitmq
`min-cluster-size: 3` (D-009 amendment -- avoids the D-062-class formation race); (B) `cluster_count:
3` on the 12 haclusters + vault-hacluster (base pins 1 = decorative); (C) **re-declares
`vault-hacluster`** (it is COMMENTED OUT by BUNDLEFIX-002) and re-adds `[vault:ha, vault-hacluster:ha]`
(D-121 v-a keeps vault MySQL-backed at 3). YAML valid; app names cross-checked vs bundle.yaml;
repo-lint clean; `provider-bundle-check.py` green on the base.

## Tests / verification

- `cd opentofu && tofu validate` -> Success; `bash scripts/opentofu-validate.sh` -> 11/11 PASS.
- `bash tests/dc-edge-wan-import/run-tests.sh` -> 58/58 PASS.
- `bash scripts/repo-lint.sh` -> 0 fail, 1 legacy WARN.
- `bash scripts/run-tests-all.sh` -> GAUNTLET: ALL GREEN (61 harnesses).

## What remains operator-gated / to bind later (the residual for a smooth batch)

1. **D-123 ruling** (Model A/B) -- unblocks wiring the `vvr1-dc0` headend + `maas-vm-host` (Step 9).
2. **Runtime prep before the edge apply:** `scripts/opnsense-prep-image.sh` to produce the nano at the
   `base_volume_path`; the netbox `--commit` to register the two /24s (ready command in the importer's
   own header; token operator-held).
3. **Stage-4 binding for the HA overlay:** render `{{VR1_DC0_CONTROL_0N}}` to the measured MAAS machine
   IDs; the base `machines:` block + the already-3 services (mysql/ovn/ceph-mon) + memcached +
   nova-compute(->2)/ceph-osd(->3) re-bind to the Option C nodes; confirm `--overlay ... --dry-run`
   (the `to:` list-replace merge is the one unverified overlay mechanism).
4. **D-121 verifies:** vault charm MySQL-backend HA at 3 units (else Raft via D-068, not etcd);
   hacluster `cluster_count` semantics.

## Revert

- `git rm -r opentofu/modules/site-wan`; revert the `opentofu/main.tf` Stage-3 section + the
  `vr1_dc1_planes` comment; `rm .terraform.lock`-nothing (lock unchanged).
- `git rm netbox/dc-edge-wan-import.py && git rm -r tests/dc-edge-wan-import`.
- `git rm overlays/dc-ha-scaleup.yaml`.
- Delete the `## D-123:` section from `docs/design-decisions.md`.
- No cloud state was touched -- nothing to unwind on the host.
