READ-ONLY planning artifact. Container-layer-elimination pass, Phase 0, Worker 1. Repo HEAD at write time: branch dc-dc-stage5-preconditions. All refs are path:line against files as read this session; re-check line numbers if the file has moved since.
vcloud (outer libvirt, qemu:///system) <- OUTER ROOT: opentofu/main.tf
|
|-- module.office1_storage / office1_network / office1_opnsense (Office1 -- NOT container-layer)
|-- module.voffice1 (Office1 headend VM -- NOT container-layer)
|-- module.ubuntu_noble_base (shared base image, multi-consumer)
|-- module.mesh_vr1_dc0_vr1_dc1 / _office1 / vr1_dc1_office1 (D-100 mesh triangle -- outer, persists)
|-- module.netem_vr1_dc0_vr1_dc1 (tc netem on a mesh bridge -- outer, persists)
|-- module.vr1_dc0_storage / vr1_dc1_storage (outer per-DC pool -- holds ONLY the containment VM's own disk)
|-- module.vr1_dc0_uplink / vr1_dc1_uplink (D-125 vcloud-level ISP NAT /24 -- the ONE real NAT egress)
|
|-- module.vvr1_dc0 <==== CONTAINMENT VM (D-122/D-123 Model B) ====
| cloudinit-vm: 108 vCPU / 480 GiB / ~3000 GiB disk (opentofu/variables.tf:137-156)
| NIC1 -> mesh_vr1_dc0_office1 (transit, region-facing, SEC-010 keys here)
| NIC2 -> vr1_dc0_uplink (IP-less bridge port -> br-vr1-dc0-wan)
| expose_nested_virt = true (LOAD-BEARING: svm passthrough for inner KVM)
| |
| |==== BOOTSTRAP GATE (scripts/site-headend-install.sh, node-host mode) ====
| | installs: nested libvirtd, inner storage-pool dir + AppArmor grant,
| | kvm nested=1, stages OPNsense base image, verifies bridge + SEC-010
| | FORWARD-drop scoped to the transit NIC. NOT an OpenTofu artifact --
| | a bash script gate between the two tofu roots.
| |
| +-- INNER ROOT: opentofu/vr1-dc0-substrate/ (provider = qemu+ssh -> vvr1-dc0, R-5)
| |-- module.inner_storage (dc-storage-pool, INSIDE vvr1-dc0)
| |-- module.vr1_dc0_planes (dc-planes x6 -- the RELOCATED planes, D-123 Model B)
| |-- module.vr1_dc0_wan (wan-bridge -> br-vr1-dc0-wan, D-125 bridge-in)
| |-- module.vr1_dc0_opnsense (opnsense-edge, LAN=provider-public, WAN=vr1_dc0_wan)
| +-- module.vr1_dc0_node[9] (node-vm x9: 3 control+2 compute+4 storage,
| PLUS juju-01, maas-01, tailscale-01 =
| 12 for_each entries total, D-104/D-132/D-129(iii))
|
+-- module.vvr1_dc1 <==== CONTAINMENT VM (mirror of vvr1_dc0) ====
cloudinit-vm: 108 vCPU / 480 GiB / ~3000 GiB disk (opentofu/variables.tf:175-194)
NIC1 -> mesh_vr1_dc1_office1, NIC2 -> vr1_dc1_uplink
|
|==== BOOTSTRAP GATE (same script, dc1 target) ====
|
+-- INNER ROOT: opentofu/vr1-dc1-substrate/ (provider = qemu+ssh -> vvr1-dc1)
mirrors dc0's inner root exactly: inner_storage, vr1_dc1_planes (x6),
vr1_dc1_wan, vr1_dc1_opnsense, vr1_dc1_node[N] (schematic MAC scheme
52:54:01:d1:NN:PP, pre-pinned at authoring -- opentofu/vr1-dc1-substrate/main.tf:84-119)
Target (post-elimination, NOT yet confirmed -- Phase-0 W0.4's job, referenced here only for contrast): vcloud (libvirt) -> node VMs directly, planes/wan/edge modules re-homed onto the outer root or vcloud itself, containment VM + bootstrap gate + inner root deleted.
opentofu/)opentofu/main.tf -- the whole outer root. Load-bearing container-layer blocks:
module "vvr1_dc0" at main.tf:410-519 -- the containment VM itself (cloudinit-vm).module "vvr1_dc1" at main.tf:537-623 -- mirror for dc1.module "vr1_dc0_uplink" at main.tf:379-384, module "vr1_dc1_uplink" at main.tf:391-396 -- the D-125 vcloud-level ISP NAT that the containment VM's NIC2 bridges into (site-wan module; this is the SOLE real internet egress for each DC).module "vr1_dc0_storage" (main.tf:35-39) / vr1_dc1_storage (main.tf:49-53) -- outer per-DC pool. Post-Model-B this pool holds ONLY the containment VM's own boot disk/seed, NOT node disks (those moved to inner_storage in the inner root).module "mesh_vr1_dc0_vr1_dc1" / _office1 / mesh_vr1_dc1_office1 (main.tf:125-141) -- the D-100 mesh triangle. dc0/dc1<->office1 legs carry the D-124 transit the containment VM's NIC1 rides; NOT container-layer-specific themselves (mesh survives elimination, only what rides it changes -- see W0.2 network map).module "netem_vr1_dc0_vr1_dc1" (main.tf:353-358) -- tc netem on mesh bridge virbr5; outer, independent of the containment layer.main.tf:22-33 -- explicitly documents the D-123 Model B move: the six vr1-dc0 planes moved to the inner root because "a libvirt provider cannot be configured from a resource created in the same apply."main.tf:300-331 -- names the apply order and the bootstrap-gate script explicitly (quoted in section 3 below).moved blocks main.tf:275-298 -- state-address rewrites from the pre-D-119/pre- Model-B naming; historical, not currently container-layer-live, but relevant if Phase 1 needs precedent for a future moved{} migration when the layer collapses.opentofu/variables.tf -- sizing/addressing vars for the containment VMs:
vvr1_dc0_vcpu/_memory_mib/_disk_bytes (variables.tf:137-156, defaults 108 / 491520 MiB / 3,221,225,472,000 bytes) and the dc1 mirror (variables.tf:175-194) -- derived via scripts/dc-dc-whole-host-budget.py (comment cites the derivation, not invented).vr1_dc0_rack_metal_admin_ip / _transit_ip / _transit_prefix / _transit_peer_ip (variables.tf:226-244) and the dc1 mirror (variables.tf:201-219) -- NO defaults, HELD values, populated from opentofu/d124-rack.auto.tfvars (gitignored; confirmed content read this session -- dc0 10.12.8.2 / 172.31.0.2/30 peer 172.31.0.1; dc1 10.12.68.2 / 172.31.0.6/30 peer 172.31.0.5).vr1_dc0_ssh_pubkey_path / vr1_dc1_ssh_pubkey_path (variables.tf:105-114,158-168) -- D-126 per-env dedicated SSH key each containment VM's cloud-init authorizes; the inner root's qemu+ssh provider authenticates with the matching private half.vr1_dc0_planes / vr1_dc1_planes (variables.tf:39-86) -- the six-plane CIDR maps. Declared in the OUTER root's variables.tf but consumed by the INNER root's dc-planes module call, not the outer root itself (outer no longer instantiates dc-planes for either DC) -- the outer var is the values-of-record copy the inner tfvars mirrors (comment variables.tf:70-73).opentofu/d124-rack.auto.tfvars -- gitignored; the four rack-addressing values per DC (metal-admin IP, transit IP/prefix/peer) that key the containment VM's netplan AND the inner root's qemu+ssh connection variable (vvr1_dc0_transit_ip in the inner root is a SEPARATE var populated from the SAME measured IP, not auto-derived from this file -- confirmed by reading vr1-dc0-substrate/variables.tf:4-7, which is a plain string var with no default).
opentofu/vr1-dc0-substrate/main.tf (266 lines, read in full):
provider "libvirt" block (main.tf:12-22) -- the qemu+ssh dial into vvr1-dc0, keyfile+sshauth REQUIRED (measured trap, no default identity works).module "inner_storage" (main.tf:26-30) -- dc-storage-pool INSIDE vvr1-dc0.module "vr1_dc0_planes" (main.tf:34-40) -- the six RELOCATED planes (dc-planes module reused verbatim from ../modules/).module "vr1_dc0_wan" (main.tf:51-56) -- wan-bridge onto br-vr1-dc0-wan.module "vr1_dc0_opnsense" (main.tf:60-73) -- the DC edge, opnsense-edge module.locals.vr1_dc0_node_nics (main.tf:77-85) + locals.vr1_dc0_nodes map (main.tf:96-247) -- 12 entries: 9 D-121 Option-C role nodes (3 control/2 compute/4 storage) + vr1-dc0-juju-01 (D-104 amendment) + vr1-dc0-maas-01 (D-132(iii)) + vr1-dc0-tailscale-01 (D-129(iii)) -- all MAC-pinned from live measurement.module "vr1_dc0_node" (main.tf:250-265) -- for_each over the 12-entry map, instantiating ../modules/node-vm once per node.opentofu/vr1-dc0-substrate/variables.tf (48 lines) -- vvr1_dc0_transit_ip / vvr1_dc0_ssh_user / vvr1_dc0_ssh_keyfile (connection vars, MEASURED post-outer- apply, no defaults on the IP/keyfile), inner_pool_path (default /var/lib/libvirt/vr1-dc0-inner), opnsense_base_path (default staged path, must be on the EXECUTING host's filesystem -- measured trap, see variables.tf:26-33), domain_suffix, underlay_mtu, vr1_dc0_planes (mirrors outer var).opentofu/vr1-dc0-substrate/versions.tf -- separate terraform{} block, same provider pin (dmacvicar/libvirt 0.9.8) as the outer root's versions.tf.opentofu/vr1-dc1-substrate/{main,variables,versions}.tf -- structural MIRROR of the dc0 inner root (confirmed by reading both main.tf files side by side): same module set (inner_storage, vr1_dc1_planes, vr1_dc1_wan, vr1_dc1_opnsense, vr1_dc1_node[for_each]), only the addressing/MAC values differ (dc1 uses a DETERMINISTIC pre-assigned MAC scheme 52:54:01:d1:NN:PP, main.tf:84-98, vs dc0's measured-and-pinned 52:54:00:* values -- a genuine divergence in HOW the MACs were assigned, not a container-layer-elimination-relevant difference).Reused VERBATIM by both the outer root (for the containment VM itself) and the inner roots (for what runs inside it) -- same module source, different provider/root:
| Module | Used by outer root for... | Used by inner root(s) for... |
|---|---|---|
modules/cloudinit-vm |
the containment VM (vvr1-dc0/vvr1-dc1) itself, AND voffice1/office1 edge (non-container uses) | not used inside |
modules/dc-storage-pool |
outer per-DC pool (now holds only the containment VM's own disk) | inner_storage (holds node + edge disks) |
modules/dc-planes |
NOT instantiated (retired outer call, per main.tf:44-47 comment) |
vr1_dc0_planes / vr1_dc1_planes -- the six planes, now INSIDE the containment VM |
modules/wan-bridge |
not used | vr1_dc0_wan / vr1_dc1_wan -- D-125 bridge-in onto the containment VM's own br-vr1-dc0-wan bridge |
modules/opnsense-edge |
office1_opnsense (non-container) | vr1_dc0_opnsense / vr1_dc1_opnsense -- the DC edge, now INSIDE the containment VM |
modules/node-vm |
not used (node VMs never lived on vcloud) | vr1_dc0_node[*] / vr1_dc1_node[*] -- all 12+12 node VMs |
modules/site-wan |
vr1_dc0_uplink/vr1_dc1_uplink -- the OUTER vcloud-level ISP NAT (persists post-elimination; not container-specific) |
office1-wan is also this module (non-container) |
modules/mesh-link |
the D-100 mesh triangle (persists; carries the transit the containment VM currently uses) | not used |
modules/netem-link |
dc0<->dc1 mesh netem (persists) | not used |
modules/base-image |
shared Ubuntu noble base (persists; containment VM boots from it via cloudinit-vm) | not used |
modules/maas-vm-host |
NOT instantiated anywhere (DEFERRED/REFUTED, see its own header: snap MAAS cannot open the local libvirt socket, qemu+ssh pod fails domblkinfo) |
same -- refuted for DC use by measurement 2026-07-20 |
modules/office1-network |
office1-local L2 (non-container, Office1-only) | not used |
None of modules/{cloudinit-vm,dc-storage-pool,dc-planes,wan-bridge,opnsense-edge, node-vm,site-wan,mesh-link,netem-link,base-image} contain any hardcoded reference to "vvr1-dc0"/"containment" in their own HCL -- they are generic and reused verbatim. The container-layer-SPECIFIC facts live entirely in the two ROOTS (which provider/state each module instance is created under) and in the containment VM's own module call (vvr1_dc0/vvr1_dc1 in the outer root). This is exactly what main.tf:326-330's comment states: "Their HCL is UNCHANGED -- the same node-vm / site-wan / opnsense-edge / dc-planes modules are reused verbatim; only the provider they run against moved from vcloud to vvr1-dc0."
main.tf:326-330)Verbatim from the outer root's own comment:
"Apply order: OUTER (this root -- boots + sizes vvr1-dc0) -> BOOTSTRAP GATE (site-headend-install.sh: install nested libvirtd + inner pool + kvm nested=1 + stage the opnsense base image) -> INNER root (planes/wan/edge/nodes)."
Concretely, three distinct steps with two different tools:
opentofu/ root, qemu:///system on vcloud, run per D-128 Plane 1 from vcloud itself, confirmed docs/CURRENT-STATE.md:159,178) -- creates vvr1_dc0/vvr1_dc1 (the containment VM), the D-125 uplink NAT, the mesh legs, the outer per-DC storage pool.scripts/site-headend-install.sh, "node-host mode" / --role rack) -- a bash script, NOT OpenTofu, run against the now-booted containment VM over SSH. Installs nested libvirtd, creates the inner pool directory + AppArmor grant (site-headend-install.sh:264-272), sets kvm nested=1, stages the OPNsense base image onto the EXECUTING host's filesystem (voffice1, per the inner root's opnsense_base_path var comment), and verifies the transit/uplink bridges + the SEC-010 FORWARD-drop scoped to the transit interface.opentofu/vr1-dc0-substrate/ or vr1-dc1-substrate/, qemu+ssh dialed from the EXECUTING host -- per D-128 the inner tofu roots run from voffice1, confirmed docs/CURRENT-STATE.md:3678 "D-128 has it run the INNER tofu roots and tofu init leaves a provider cache") -- creates the inner storage pool, the six relocated planes, the WAN bridge, the DC edge, and the 12 node VMs per DC.This ordering is STRUCTURALLY FORCED, not a preference: main.tf:28-29's comment states the reason plainly -- "A libvirt provider cannot be configured from a resource created in the same apply, so the inner substrate is a separate root/state applied AFTER vvr1-dc0 is up (the bootstrap gate)." Eliminating the containment VM removes this structural forcing function entirely -- a flat topology would let ONE root (or a set of modules under one root/state) create the planes/edge/nodes directly on vcloud's own qemu:///system provider, with no bootstrap-gate script and no second tofu init/apply cycle. This is very likely the single largest simplification the elimination buys, structurally speaking (Phase 1/2 should confirm the operational-workflow-step count this removes).
Two INDEPENDENT chains today, one per root:
Outer chain (containment VM's own boot disk): module.ubuntu_noble_base (base-image, downloaded once, main.tf:165-173) --backing_store--> module.vvr1_dc0/vvr1_dc1 (cloudinit-vm) which creates, per instance:
libvirt_volume.disk (COW backing off the shared base image, modules/cloudinit-vm/main.tf:24-47)libvirt_cloudinit_disk.seed + libvirt_volume.seed (the NoCloud ISO, modules/cloudinit-vm/main.tf:49-76) -- lifecycle { ignore_changes = [create] } guards against the D-130 staging-path-remint-on-reboot trap.module.vr1_dc0_storage/vr1_dc1_storage (the OUTER per-DC pool, dc-storage-pool, main.tf:35-53).Inner chain (everything the containment VM hosts):
module.inner_storage (dc-storage-pool, vr1-dc0-substrate/main.tf:26-30) is a FRESH pool created at var.inner_pool_path (default /var/lib/libvirt/vr1-dc0-inner) -- a directory the BOOTSTRAP GATE (not OpenTofu) provisions on the containment VM's own filesystem before this root can apply.module.vr1_dc0_node[*] (node-vm) creates a BLANK libvirt_volume.disk per node (PXE-boot pattern, no backing image -- modules/node-vm/main.tf:44-62) plus an opt-in OSD volume for storage-role nodes, all in inner_storage.module.vr1_dc0_opnsense (opnsense-edge) creates its disk as a DIRECT COPY (not COW) of var.opnsense_base_path (modules/opnsense-edge/main.tf:55-85) -- a file that must exist on the EXECUTING host's filesystem (the qemu+ssh provider uploads from its own client-side filesystem, NOT from a path on vvr1-dc0 -- measured trap, vr1-dc0-substrate/variables.tf:26-33), also placed in inner_storage.Key coupling for elimination planning: the inner chain's inner_storage pool is NOT the same pool as the outer vr1_dc0_storage/vr1_dc1_storage pool -- they are two separate dc-storage-pool module instances under two separate roots/providers, with the inner one's target directory living INSIDE the containment VM's own disk image (itself a volume in the outer pool). A flat topology collapses this to ONE pool per DC on vcloud's own filesystem holding node/edge/pool volumes directly -- no nested-disk-inside-a-disk indirection, and no bootstrap-gate-provisioned directory dependency.
Would be REMOVED entirely if the containment VM is eliminated:
module "vvr1_dc0" / module "vvr1_dc1" (opentofu/main.tf:410-519, 537-623) -- the containment VM domain + its disk + seed volumes.opentofu/vr1-dc0-substrate/{main,variables,versions}.tf and opentofu/vr1-dc1-substrate/{main,variables,versions}.tf as separate roots/states (their MODULE CALLS would likely be re-homed into the outer root or a new flat root, not deleted -- see below).
module.inner_storage in each inner root -- re-homed (folds into, or replaces, the outer per-DC storage pool).scripts/site-headend-install.sh "node-host mode" -- nested-libvirtd / inner-pool / kvm-nested=1 install -- has NOTHING to install once there is no nested hypervisor.qemu+ssh provider connections (vr1-dc0-substrate/main.tf:11-22, vr1-dc1-substrate/main.tf:11-19) and their connection variables (vvr1_dc0_transit_ip/_ssh_user/_ssh_keyfile and dc1 mirror) -- a flat topology needs no remote-libvirt dial at all; the outer root's qemu:///system covers everything.module "vr1_dc0_wan" / vr1_dc1_wan (wan-bridge module calls) -- these exist ONLY to bridge the inner OPNsense edge onto the containment VM's own br-vr1-dc0-wan netplan bridge; with no containment VM there is no such bridge to attach to. The DC edge's WAN would need to reconnect DIRECTLY to vr1_dc0_uplink/vr1_dc1_uplink (the outer D-125 NAT network) instead -- likely reverting toward the pre-Model-B ("Model A") shape docs/model-a-fallback-plan.md already describes as the fallback.main.tf:495-518, 599-621): the br-vr1-dc0-wan/br-vr1-dc1-wan bridge declarations, the transit-leg static addressing consumed only by the inner qemu+ssh dial, and the D-124 rack-addressing vars that exist ONLY to address the containment VM (vr1_dc0_rack_transit_ip/_prefix/_peer_ip, vr1_dc0_rack_metal_admin_ip, and dc1 mirrors -- variables.tf:196-244).vvr1_dc0_vcpu/_memory_mib/_disk_bytes and dc1 mirror (variables.tf:137-156, 175-194) -- sizing vars that exist only because the containment VM must hold the ENTIRE node fleet (104 node vCPU + 4-96 GiB overhead derivation). A flat topology sizes each node VM directly against vcloud's own budget instead (no "containment overhead" line item at all -- scripts/dc-dc-whole-host-budget.py's --containment-overhead-* flags would need re-deriving or dropping).vr1_dc0_ssh_pubkey_path/vr1_dc1_ssh_pubkey_path (D-126 per-env keys) and the ~/vr1-dc0-creds/, ~/vr1-dc1-creds/ key material they authorize -- these exist specifically to authenticate the inner qemu+ssh provider to the containment VM; a flat topology has no SSH-to-a-nested-hypervisor step to key.Would be RE-HOMED (kept, but moved to the outer root / vcloud's own provider), not deleted:
module "vr1_dc0_planes" / vr1_dc1_planes (dc-planes x6 each) -- the six planes themselves are NOT container-layer artifacts; only WHERE they are created (inside vs. outside the containment VM) is. This is precisely the D-123 Model B move already documented at main.tf:22-33 and CURRENT-STATE.md:7676-7682 (2.3-iii) -- eliminating the container layer is largely UNDOING that specific move: planes go back to being created directly by the outer root (or a flat per-DC root) on vcloud's own libvirt, the way they stood before D-123 Model B (Model A).module "vr1_dc0_opnsense" / vr1_dc1_opnsense (opnsense-edge) -- the DC edge VM itself; only its WAN attachment (currently vr1_dc0_wan/wan-bridge) needs to change to attach directly to the outer vr1_dc0_uplink/vr1_dc1_uplink NAT network instead.module "vr1_dc0_node" / vr1_dc1_node (for_each node-vm, 12 entries each) -- the node VMs themselves; only their provider (containment-VM qemu+ssh -> vcloud qemu:///system) and their storage-pool parent (inner_storage -> a per-DC pool on vcloud) change.module.inner_storage -- effectively MERGES with (or is replaced by) the existing outer vr1_dc0_storage/vr1_dc1_storage pool, since there is no longer an "inner" vs "outer" distinction once everything is on vcloud.Would be UNCHANGED / NOT container-layer-specific (persist regardless):
module "voffice1", office1_opnsense, office1_network, office1_storage, ubuntu_noble_base -- Office1 headend, untouched.module "mesh_vr1_dc0_vr1_dc1", mesh_vr1_dc0_office1, mesh_vr1_dc1_office1 (mesh-link) and netem_vr1_dc0_vr1_dc1 -- the D-100 mesh triangle. NOTE for W0.2: what RIDES the office1<->dc0/dc1 mesh legs changes (today: the transit to the containment VM's NIC1; post-elimination: whatever replaces the region<->rack path, if anything is still needed once there is no separate rack/region split) -- flagged as a W0.2 item, not resolved here.module "vr1_dc0_uplink" / vr1_dc1_uplink (site-wan, the D-125 vcloud-level ISP NAT) -- persists as the DC's egress; only what attaches to it changes (today: the containment VM's bridge port; post-elimination: directly the DC edge's WAN NIC, likely reverting to a site-wan-module-style NAT attachment matching Office1's own pattern).modules/maas-vm-host -- already NOT instantiated for either DC (refuted by measurement, unrelated to this pass).wan-bridge module) has no proven Model-A equivalent in this codebase's CURRENT config -- Model A's original NAT-based WAN shape was RETIRED when Model B replaced it (main.tf:44-47's comment: "the old pre-Model-B module vr1_dc1_planes skeleton... is retired -- do not re-add"). Reverting needs either resurrecting that retired shape or re-deriving it; docs/model-a-fallback- plan.md exists and should be W0.4's/Phase-1's first read, not re-invented.virsh domiflist on vvr1-dc0/vvr1-dc1). Re-homing these for_each maps to a new provider will very likely register as a -/+ (force-replace, since name is ForceNew per the existing moved{} block precedent at main.tf:256-271) or at minimum demands re-measuring and re-pinning 12x2=24 nodes' MACs plus 2 edges' MACs after the move -- MAAS re-enlistment risk is real and large, not cosmetic (the exact "MAC regeneration trap" incident class this repo already suffered once, per multiple comments cross-referenced above).qemu:///system (vcloud) provider is even the RIGHT single target is a W0.4/operator question, not resolved here -- this doc only maps what exists; it takes no position on the target topology.site-headend-install.sh) has a SECOND role (region+ rack MAAS enrollment, SEC-010 nftables) beyond nested-libvirt bring-up -- eliminating the containment layer removes the nested-libvirt PART of its job but the script likely still needs SOME of its other duties (rack MAAS role, SEC-010) reassigned somewhere; W0.3 (consumer inventory) owns confirming exactly which parts survive.underlay_mtu (jumbo, 9000) assumptions inside the currently- nested plane networks change once they run on vcloud's own bridges directly (host NIC MTU vs. a nested-guest's virtio MTU) -- geneve-over-v6 budget is explicitly a W0.2 deliverable per phase-prompts.md; this doc does not check it.