Author: W2.1 (Phase-2 sonnet worker, SCOPE-AND-EXECUTION-PLAN.md Section 4). Date: 2026-08-09. READ-ONLY planning; no mutation. Baseline consumed in full: SCOPE-AND-EXECUTION-PLAN.md, pass0-admin-report.md (Option 1 CONFIRMED at the Phase-0 gate; cross-DC handling (a) CONFIRMED), pass1-admin-report.md (planning change-set; root topology left explicitly OPEN for this worker to design, Phase 4 to ratify -- Section 7 item 1
opentofu/main.tf, opentofu/variables.tf, opentofu/vr1-dc0-substrate/main.tf, opentofu/vr1-dc1-substrate/main.tf, and every file under opentofu/modules/*.Three roots, 16 module call sites, 12 distinct module types:
opentofu/main.tf, 623 lines, qemu:///system local to vcloud): DC storage pools (x3, incl. office1), office1-network, office1-opnsense, 3 mesh-triangle legs, netem, base-image, voffice1 (cloudinit-vm), 2 site-wan uplink NATs, vvr1_dc0/vvr1_dc1 (cloudinit-vm -- the containment VMs, :410-519/:537-623).opentofu/vr1-dc0-substrate/main.tf, vr1-dc1-substrate/main.tf, qemu+ssh into vvr1-dcN): inner storage pool, 6 planes (dc-planes), wan-bridge, DC edge (opnsense-edge), 12 node VMs (node-vm for_each: 9 D-121 role nodes + juju-01 + maas-01 + tailscale-01).opentofu/modules/*: base-image, cloudinit-vm, dc-planes, dc-storage-pool, maas-vm-host, mesh-link, netem-link, node-vm, office1-network, opnsense-edge, site-wan, wan-bridge.| Module | Current call sites (path:line) | Option-1 disposition | Reusable-module shape / notes |
|---|---|---|---|
cloudinit-vm |
voffice1 (main.tf:175), vvr1_dc0 (:410), vvr1_dc1 (:537) |
PARTIAL COLLAPSE + NEW USE. The two containment-VM instantiations (vvr1_dc0/vvr1_dc1) DELETE with the containment layer. The module BODY is unchanged and gains a NEW instantiation: the vr1-dcN-client VM (pass0's Option-1 shape: ~4/8192/80, metal-admin + transit legs, expose_nested_virt=false) is "L1, same module type as voffice1/edges" (pass1 Section 5). voffice1's call is untouched (D-114, out of scope, pass1 check 1). |
Stays a leaf module; no interface change needed. The client VM's network_names/network_config need a metal-admin plane ref (new -- containment VMs never touched a plane directly) instead of an uplink NIC. |
dc-planes |
vr1_dc0_planes (inner :34-40), vr1_dc1_planes (inner :32-38) |
RE-HOME, body unchanged. Moves from the inner (qemu+ssh) root to the flat per-DC root, still one call per DC, same planes map / mtu / domain_suffix inputs (D-139/D-143 own the CIDR values, not this module). |
No shape change. Becomes a direct child of the new dc-site composing module (Section 3). |
dc-storage-pool |
vr1_dc0_storage/vr1_dc1_storage (outer :35-39/:49-53, held ONLY the containment VM's own disk), office1_storage (outer :59-63), inner_storage x2 (inner :26-30/:23-27, held the 12 node disks + edge disk) |
COLLAPSES 2-per-DC -> 1-per-DC. The outer/inner split existed only because Model B had two disks per DC (containment VM's own + the pool inside it). With no containment VM there is one disk surface per DC: the current inner_storage call (renamed) becomes THE per-DC pool, sized to hold node disks + edge disk + the client VM's disk. office1_storage is untouched (D-114). |
Module body unchanged (a libvirt_pool type=dir). Net effect: 4 DC-scoped pool calls today -> 2 (one per DC) + office1_storage unchanged = 3 total, down from 5. |
maas-vm-host |
NONE -- authored but never instantiated (REFUTED for DC use by measurement 2026-07-20; main.tf:6-19 of the module itself; retained pending the SEC-013 retire-or-keep ruling) |
NO CHANGE -- orthogonal to container-elim. MAAS discovery under Option 1 is still PXE-enlistment + per-machine power_type=virsh (pass1 check 7, D-103/D-123 amendments), never this module. Flattening does not resurrect it. |
Flag only: this pass is a natural moment to close the SEC-013 retire-or-keep question, but that ruling belongs to whoever owns SEC-013, not this pass -- listed so it is not silently forgotten, not acted on here. |
mesh-link |
mesh_vr1_dc0_vr1_dc1, mesh_vr1_dc0_office1, mesh_vr1_dc1_office1 (outer :125-141) |
STAYS, all 3 legs, module and call sites unchanged. This resolves pass1's open item 6 at the module-survey level: the mesh triangle is not containment-layer at all (it wires DC<->DC and DC<->Office1 L2 segments, independent of who lives at each end). What changes is the CONSUMER on the two office1-legs: vvr1_dc0/vvr1_dc1 NIC1 today -> the vr1-dcN-client VM's transit NIC under Option 1 (client VM inherits the D-138/D-124 transit-leg role). The dc0<->dc1 leg (netem target) is untouched either way. |
No module change. The purpose of the surviving transit leg (ssh -J / Office1-originated flows / SEC-010-successor-(ii) endpoint) is still an open placement question (pass1 Section 7 item 4) -- a consumer-side decision, not a module-shape one. |
netem-link |
netem_vr1_dc0_vr1_dc1 (outer :353-358) |
STAYS, untouched. Targets the dc0<->dc1 mesh bridge directly (virbr5), runs local tc on vcloud (D-128) -- has no dependency on either DC's internal shape. Confirmed non-consumer, same class as lib-net.sh in pass0's row 15. |
No change. |
node-vm |
vr1_dc0_node/vr1_dc1_node for_each (inner :250-265/:214-229) |
RE-HOME, body unchanged. The single largest re-home by resource count (12 domains/DC): the for_each map + locals block move verbatim from the inner root into the flat per-DC root, attaching to the same dc-planes outputs (now created locally, not over qemu+ssh). MAC re-measurement is owed post-apply regardless (pass1 Section 6 item 8) -- a data concern, not a module-shape one. |
No shape change. Becomes a direct child of dc-site. |
office1-network |
office1_network (outer :76-80) |
STAYS, untouched. D-114/Stage-2 territory (pass1 check 1). | No change. |
opnsense-edge |
office1_opnsense (outer :99-117), vr1_dc0_opnsense/vr1_dc1_opnsense (inner :60-73/:56-69) |
RE-HOME (DC calls only), body unchanged, one input rewired. The two DC-edge calls move to the flat per-DC root. wan_network_name currently takes module.vr1_dc0_wan.network_name (the now-dead wan-bridge); it becomes module.vr1_dc0_uplink.network_name (the outer root's site-wan NAT) directly -- literally the same string-typed input, no module-interface change, just a different upstream module feeding it (D-125 bridge-in removed, "direct-NAT" per pass0 Section 4). office1_opnsense untouched. |
No shape change to the module itself. Precedent for cross-root string-valued network references already exists in this repo: office1_opnsense's wan_network_name = "office1-wan" (main.tf:112) is a bare literal today, not even a module reference -- the DC edges' rewired input is the SAME pattern, just sourced from a real module output instead of a literal. |
site-wan |
vr1_dc0_uplink/vr1_dc1_uplink (outer :379-384/:391-396) |
STAYS, unchanged, gains a consumer. Same 2 calls, same 172.30.2.0/24/172.30.3.0/24 (ruled literals, D-125 note at variables.tf:246-251 -- IPAM identity untouched). Its output now feeds the DC edge's WAN directly (see opnsense-edge row) instead of via wan-bridge. |
No change. This module absorbs the egress duty wan-bridge used to hand off. |
wan-bridge |
vr1_dc0_wan/vr1_dc1_wan (inner :51-56/:45-50) |
COLLAPSES / DELETED. Its entire reason to exist (D-125 bridge-in) was that the containment VM's only routed leg was the SEC-010 FORWARD-dropped transit, so a NAT-mode WAN inside it would have had no egress (OBS-3). With no containment VM, the DC edge attaches directly to the outer site-wan NAT -- there is nothing left for this module to bridge. Confirmed dead per pass0 row 3 and pass1's B.4 "VANISHES" list. |
Retire the module directory (or leave in place, unreferenced, per this repo's append-only bias -- Phase 4's call, not this pass's). Its host-bridge (br-vr1-dcN-wan) and the bootstrap's --host-nodes bridge-verify duty vanish with it. |
base-image |
ubuntu_noble_base (outer :165-173) |
STAYS, unchanged, gains consumers. One call today, consumed by voffice1 + both vvr1_dcN. Under Option 1 it is consumed by voffice1 + both vr1-dcN-client VMs (same base image, same backing_store copy-on-write pattern) -- one fewer distinct consumer class (containment VMs gone) but the SAME call. |
No change. |
Summary count: of 12 module types, 1 collapses (wan-bridge), 1 is dead/orthogonal (maas-vm-host), 3 stay untouched (office1-network, mesh-link, netem-link), 1 stays with a rewired input (site-wan), and 6 re-home with unchanged bodies (cloudinit-vm, dc-planes, dc-storage-pool, node-vm, opnsense-edge, base-image). Zero modules need their HCL rewritten -- every disposition is a re-homing / re-wiring / deletion of call sites, never a module-body change. This is the direct payoff of the modules already being written provider-agnostically (D-119's own design intent) plus the fact that flattening does not change what gets built, only where the provider dials.
Carried open from Phase 1 (pass1-admin-report.md Section 1 check 2, Section 7 item 1): merged single flat root vs. shared-outer + per-DC roots. Pass 1 established the sequence and the (a)-control ordering hold under EITHER shape, and explicitly assigned this worker to DESIGN it (Phase 4 ratifies).
opentofu/ directory/state: mesh, uplinks, office1, voffice1, BOTH DCs' pools/planes/edges/12-node fleets/client VMs, all in one state file, ideally driven by a single for_each over a var.sites map ({vr1-dc0 = {...}, vr1-dc1 = {...}}).provider "libvirt" block changes from qemu+ssh://...@${transit_ip}/system?keyfile=... (inner :12-22, dc1 :11-19) to the outer root's own local qemu:///system (variables.tf:1-4) -- and it additionally gains that DC's vr1-dcN-client VM (moved out of the outer root, see Section 2.4).State blast radius. (A) puts every domain of both DCs -- the full node fleet, both edges, both client VMs' credentials-bearing config -- in ONE state file. An untargeted mistake (the exact incident class CLAUDE.md's hard rule 4 preamble names: "an ad-hoc juju destroy-model was run instead of the D-061 teardown scripts and took the dc0 controller down") threatens both DCs at once. (B) makes DC0's and DC1's states structurally un-reachable from each other -- a mistake in DC1's root cannot touch a DC0 resource because it is not in DC1's state file, full stop, with no reliance on -target discipline being followed correctly every time. (B) wins decisively -- this axis is where this repo has first-hand incident history to weigh against.
Destroy scoping / the teardown primitive. D-122's original intent was "site-down = one virsh destroy vvr1-dc0" -- ONE OBJECT, ONE COMMAND. Under (B), the closest re-earn is literally cd opentofu/vr1-dc0-flat/ && tofu destroy (or a thin wrapper script) -- one directory, one command, matching the ORIGINAL intent almost exactly, and matching this repo's EXISTING convention (the inner roots are already per-DC directories; dc-dc-teardown- rollback.md Path A is already organized around "pick a root"). Under (A), site-down requires a generated -target=module.xxx list (one entry per plane/node/edge/client-VM resource for that DC) -- a new artifact to build AND keep in sync with the node roster, and a bare tofu destroy run without it wipes BOTH DCs (again, the CLAUDE.md-named incident shape). (B) wins -- lower delta from D-122's intent, reuses an existing convention, smaller new surface to build and test.
Cross-DC (a)-isolation ordering. Pass 1 Section 3's ordering invariant: the control must be verified "before ANY flat substrate apply that can make any two DCs' planes co-resident... since under a merged single root the FIRST apply may create both DCs' planes at once." Under (A) this is a real, standing risk: the default (untargeted) tofu apply of a merged root is ALREADY a both-DCs-co-resident event on its very first run, so safety depends on the operator remembering to -target down to one DC even for that very first apply. Under (B) this is structural, not procedural: DC1's planes literally cannot be created by applying DC0's root -- there is no way to accidentally co-create them, so the ordering invariant reduces to "run the (a) control's --check before the first per-DC-root apply of EITHER DC," a much simpler operator contract. (B) wins.
for_each/module reuse. Largely NEUTRAL. Regardless of root shape, every module body listed in Section 1 is reused unchanged -- a module call does not care which root instantiates it. Introducing the dc-site composing module (Section 3) gives BOTH shapes the same DRY benefit: (A) would for_each dc-site over var.sites; (B) has each per-DC root make ONE dc-site call with that DC's own tfvars file. (A) is marginally more DRY at the HCL-file level (one file instead of two near-identical directories differing only by tfvars), but (B)'s "two thin directories, one shared module" is not meaningfully more duplicative than (A)'s "one file with a for_each" once dc-site exists -- this repo already carries the inner roots' current near-duplication (dc0/dc1 substrate main.tf files today) and pass1/pass0 both identify that duplication as tolerable precisely because the MODULE bodies (not the root files) are the unit of reuse. Mild edge to (A), not decisive against the three axes above.
Current outer/inner structure. (B) is the direct, low-delta evolution of what exists today -- the inner roots ALREADY are per-DC directories; the only structural change is deleting the qemu+ssh provider indirection (one block per root) and merging the outer vvr1_dcN-disk pool into the (renamed) inner pool. (A) requires collapsing 3 roots/states into 1, which means re-deriving resource addresses for everything currently in the outer root's DC-scoped blocks (the vr1_dc0_storage/vr1_dc1_storage pools, if kept renamed, need moved{} blocks -- this repo has done exactly this kind of state-address migration once before, for a much smaller rename, in main.tf:275-298's D-119 moved{} blocks). (B) wins on migration risk and delta size.
Recommend (B): shared-outer root + per-DC-flat roots (three roots total). Every blast-radius, destroy-scoping, and isolation-ordering axis favors it, each for reasons specific to THIS repo's own operating discipline and incident history (CLAUDE.md hard rule 4's own worked example is exactly the failure mode (B) structurally prevents and (A) would reproduce at a larger scale). (A)'s only edge -- marginally more DRY HCL -- is fully absorbed by the dc-site module proposed in Section 3, which benefits both shapes equally. This recommendation is directional, for Phase 4 to ratify (pass1 Section 7 item 1), not a ruling.
What (B) implies for the teardown primitive: the emergency/gated site-down lever (pass1 Section 6 items 1 + 6) becomes "operate on that DC's root directory" -- tofu destroy (gated) or a virsh destroy loop over that root's own state-listed domains (emergency), never touching the other DC's or the outer root's state. This is the artifact Phase 2/W2.3 or W2.4 should build against.
What (B) implies for the (a)-control ordering: the control's --check gate (Stage 1 home, per pass1 Section 3) precedes "the first tofu apply of EITHER per-DC-flat root" -- a single, simple operator contract, not a per-root-shape-dependent one. No -target discipline is load-bearing for safety under (B) (unlike (A)).
Recommend: the vr1-dcN-client VM's APPLY site is the per-DC-flat root (grouped with that DC's planes/nodes/edge), even though its MODULE TYPE is cloudinit-vm (L1, same type as voffice1 -- pass1 Section 5's layer model, which classifies by module type, not apply grouping; both can hold per pass1's own reconciliation). Rationale, specific to (B):
docs/design-decisions.md:7118-7124) is honored more literally by the client VM sharing its DC's own tofu root/state than by living in a shared outer one.modules/dc-site (the composing module Option 1 wants -- HIGHEST LEVERAGE)Replaces the copy-pasted body of vr1-dc0-substrate/main.tf / vr1-dc1-substrate/main.tf (currently ~230-266 lines each, identical shape, differing only in tfvars + dc1's schematic MAC scheme) with ONE module both per-DC-flat roots call once. Composes, in order: dc-storage-pool (1) -> dc-planes (6) -> opnsense-edge (1, direct-NAT wired) -> node-vm for_each (12: 9 D-121 role nodes + juju-01 + maas-01 + tailscale-01) -> cloudinit-vm (1, the client VM).
Inputs (sketch, no invented values -- every CIDR/MAC/sizing stays a real per-DC var, same discipline variables.tf already enforces):
site_token # e.g. "vr1-dc0" -- D-106 naming anchor, threaded into every child call
domain_suffix # anchor, D-106
underlay_mtu # Phase-0 measured, no default
pool_path # host filesystem path for this DC's single storage pool
planes # map(plane_name -> {cidr}), same shape as var.vr1_dc0_planes today
opnsense_base_path # prepped nano image path (unchanged from today)
uplink_network_name # STRING -- the outer root's site-wan NAT name (e.g. "vr1-dc0-uplink"),
replaces module.vr1_dc0_wan.network_name; same cross-root string
pattern office1_opnsense already uses (main.tf:112)
nodes # map(vm_name -> {vcpu, mem, disk_gib, osd_gib?, macs}) -- the 12-entry
roster, identical shape to today's vr1_dc0_nodes/vr1_dc1_nodes locals
client_vm # object: vcpu/mem/disk, ssh_pubkey_path, macs, transit_network_name
(STRING -- outer root's mesh_vr1_dcN_office1 network name),
metal_admin_ip/transit_ip (NetBox-assigned, no defaults)
Outputs: plane network_names map (pass-through, for verify scripts / a future D-140 Juju root to consume node/edge addressing), node domain_names/ids map, client-VM domain_id, edge domain_id -- the same shape dc-planes/node-vm/cloudinit-vm already expose today, just re-exported one level up.
What it deliberately does NOT own: the (a) cross-DC isolation control (confirmed a procedure/L5 artifact, not a tofu module, pass1 Section 3 -- do not build it twice here); the rack-controller-remainder / D-131 forwarder / credential-bootstrap content of the client VM's user_data (pass1 Section 7 OPEN item 2 -- still unruled; this module's client_vm input should accept a user_data/network_config string exactly like cloudinit-vm does today, NOT hardcode content that depends on an unruled placement decision).
opentofu/vr1-dc0-flat/, opentofu/vr1-dc1-flat/ (naming: avoid reusing -substrate unqualified if that name is wanted for the pre-Roosevelt bare-metal target later -- Phase 4's naming call, flagged not decided here). Each root: one local provider "libvirt" block (no keyfile/sshauth/ known_hosts -- that whole trap class, vr1-dc0-substrate/main.tf:12-22's header comment, disappears with the qemu+ssh dial), one module "site" { source = "../modules/dc-site" ... } call, one tfvars file. This is the direct, low-delta successor described in Section 2.
site-client-vm wrapper moduleThe client VM's cloudinit-vm call could someday warrant its own thin wrapper (to standardize its user_data templating once the rack-controller-remainder + D-131 forwarder + credential bootstrap logic is designed). Not proposed now: that content depends on pass1's still-OPEN Section 7 item 2 (rack-controller-remainder placement), and building the wrapper before that ruling would either invent the content (hard rule 2 violation) or ship an empty wrapper with no real interface to validate. Flagged as a likely Phase-2/W2.4 or Phase-4 follow-on once that placement is ruled, not built here.
opentofu/modules/Restated from this dimension's view so Phase 2's tools workers do not build it twice: pass1 Section 3 already resolves its artifact kind as a procedure/L5 verify artifact (nftables + --check + harness + SEC-NNN row), not an OpenTofu resource. Nothing in this survey changes that. It sequences BEFORE the first tofu apply of either vr1-dcN-flat root (Section 2.3).
D-140 (docs/design-decisions.md:7781-7819, PINNED 2026-08-02 to the end-of-deployment review) converts the Juju layer (applications/relations -- the bundle.yaml-equivalent 56-application/108-relation surface) to OpenTofu-managed resources, sequenced strictly AFTER a hardened, tested dc0 deployment. It is an L4 concern (pass1 Section 5's layer model) and does not touch any module in Section 1 or the dc-site module proposed here -- those are all L0-L2 substrate. Where D-140 would eventually intersect this pass's deliverable, noted for later, not designed now:
opentofu/vr1-dcN-juju/, Juju-provider-backed) consuming this pass's dc-site OUTPUTS (node hostnames/addresses, once MAAS has assigned them -- not available at dc-site apply time) as its inputs, not folding into dc-site itself.<site_token>-<purpose>) to admit a future -juju root per DC without renaming the substrate roots this pass proposes.references/opentofu-provider- docs.md) before it can rule (a) model/config-layer-only, (b) full translation, or (c) decline -- none of which this pass's substrate-module design depends on or should pre-suppose.for_each DRY-ness is the only edge for a merged root and is absorbed by the dc-site module either way.site-wan) stays with a rewired input, 3 stay fully untouched, 1 (wan-bridge) collapses, 1 (maas-vm-host) is dead/orthogonal.modules/dc-site (composes pool+planes+edge+12 nodes+client VM; interface sketched Section 3.1) -- the single highest-leverage new artifact, replacing today's copy-pasted per-DC inner-root bodies.site-client-vm wrapper is explicitly NOT proposed yet -- blocked on pass1's open rack-controller-remainder ruling; building it now would invent content.opentofu/modules/ (procedure/L5, already ruled by pass1) -- restated here so Phase 2's tools workers do not duplicate it.