# Root module: wires the network + storage-pool layer for VR1 (Stage 1-2 of
# docs/dc-dc-deployment-workflow.md). See opentofu/README.md for full scope,
# what is deliberately deferred, and why.
provider "libvirt" {
uri = var.libvirt_uri
}
# ---- The provider "maas" CONFIG block is deliberately DEFERRED to Stage 3
# (DOCFIX-179, decided on the first real `tofu plan`). No module instantiated in
# Stage 1-2 uses the maas provider (modules/maas-vm-host is a Stage-3 module,
# commented out below), yet a configured provider "maas" block here forced
# `tofu plan` to demand maas_api_url AND the SENSITIVE maas_api_key for a plan
# that creates ZERO MAAS resources -- which would bake a fake key into
# plan/state (the DOCFIX-175 plaintext-secret surface). maas stays in
# versions.tf's required_providers (version pinned, lock-file complete); Stage 3
# re-adds this provider block + its two variables when it actually instantiates
# modules/maas-vm-host. modules/netem-link is likewise not instantiated yet
# (needs a real bridge/SSH target + netem params). See gap register items
# 2/4/11 and opentofu/README.md. ----
# ---- DC1: inherits the DC0 six-plane v4 layout unchanged (D-101) ----
module "dc1_planes" {
source = "./modules/dc-planes"
dc_name = "dc1"
domain_suffix = var.domain_suffix
mtu = var.underlay_mtu
planes = var.dc1_planes
}
module "dc1_storage" {
source = "./modules/dc-storage-pool"
dc_name = "dc1"
target_path = var.dc1_pool_path
}
# ---- DC2: PLANES deferred (operator ruling 2026-07-10, Option B) -- wait for
# NetBox to assign D-101's supernet/ULA/GUA before wiring dc2_planes. Do NOT
# fill in guessed CIDRs to make it "work" sooner. When NetBox assigns it, add a
# `dc2_planes` variable (same shape as `dc1_planes`) and uncomment this block.
# The storage pool + mesh legs are NOT address-dependent, so they ARE wired now.
#
# module "dc2_planes" {
# source = "./modules/dc-planes"
# dc_name = "dc2"
# domain_suffix = var.domain_suffix
# mtu = var.underlay_mtu
# planes = var.dc2_planes
# }
module "dc2_storage" {
source = "./modules/dc-storage-pool"
dc_name = "dc2"
target_path = var.dc2_pool_path
}
# ---- Office1 headend: its own storage pool for MAAS-region/NetBox/GitBucket
# service-VM disks (D-103). Office1 does not get a dc-planes module -- the
# six-plane template is per-OpenStack-DC, not per-headend. ----
module "office1_storage" {
source = "./modules/dc-storage-pool"
dc_name = "office1"
target_path = var.office1_pool_path
}
# ---- Office1-local network (closes tooling gap register item #12): the
# shared L2 segment the three Office1 service VMs (MAAS-region, NetBox,
# GitBucket), the vcloud host's own management interface, and Office1's own
# OPNsense edge's LAN side (gap #16) all attach to. See
# modules/office1-network/main.tf's header for the design decision (a new
# module, not a reused host bridge) and opentofu/README.md for the full
# writeup. Instantiated for real here (unlike office1_opnsense below): it
# needs no value beyond domain_suffix/underlay_mtu, both already real,
# required inputs elsewhere in this file -- the same reasoning that already
# lets the three mesh-link legs be instantiated today. ----
module "office1_network" {
source = "./modules/office1-network"
domain_suffix = var.domain_suffix
mtu = var.underlay_mtu
}
# ---- Office1's own OPNsense edge (closes tooling gap register item #16 --
# decision: YES, Office1 gets a fourth modules/opnsense-edge call alongside
# DC1/DC2, and Stage 2's runbook (runbooks/dc-dc-phase1-office1-standup.md)
# owns creating it -- see that runbook's new Step 4b and its "Open questions"
# section for the full reasoning). NOT instantiated below -- commented out on
# purpose, mirroring the identical NOT-YET-REAL status of module
# "dc1_opnsense"/"dc2_opnsense" (see runbooks/dc-dc-phase2-tofu-dc-substrate.md
# Step 5's own template, which is ALSO still all placeholders, nothing
# committed here either). Every value below needs to be real before this is
# uncommented -- do not fill in invented specs to wire it in sooner:
#
# Office1's own OPNsense simulated-ISP edge (gap #16 CLOSED). gap #17 resolved
# for Office1 via the `office1-wan` NAT network (created via virsh 2026-07-12 --
# a documented D-103 debt to formalize into an OpenTofu module later, same class
# as the retained `wan`). LAN = the Office1-local segment; WAN = office1-wan (its
# own simulated ISP -> internet). Config is the DOCFIX-185 real-ISP-router config
# (no egress-airgap), rendered to office1-opnsense-config.iso.
module "office1_opnsense" {
source = "./modules/opnsense-edge"
vm_name = "office1-opnsense"
memory_mib = 2048
vcpu = 2
pool_name = module.office1_storage.pool_name
base_volume_path = "/var/lib/libvirt/vr1/office1/opnsense-26.1-nano.qcow2"
# No disk_size_bytes: the disk is a direct copy of the prepped nano, so its size comes
# from scripts/opnsense-prep-image.sh's GROW (default +8G -> 11 GiB). The old
# `disk_size_bytes = 16 GiB` here was SILENTLY IGNORED -- measured: the live volume is
# 11 GiB. Removed in DOCFIX-189.
lan_network_name = module.office1_network.network_name
wan_network_name = "office1-wan"
}
# ---- The D-100 dark-fiber mesh triangle: DC1<->DC2, DC1<->Office1,
# DC2<->Office1 -- mesh, not star. All three legs created now even though DC2
# itself is not yet wired above: the LINK is independent of whether DC2's
# plane networks exist yet, and Phase 0's gate wants "virtual networks present
# and isolated as designed" before Phase 2 needs them. ----
module "mesh_dc1_dc2" {
source = "./modules/mesh-link"
link_name = "dc1-dc2"
mtu = var.underlay_mtu
}
module "mesh_dc1_office1" {
source = "./modules/mesh-link"
link_name = "dc1-office1"
mtu = var.underlay_mtu
}
module "mesh_dc2_office1" {
source = "./modules/mesh-link"
link_name = "dc2-office1"
mtu = var.underlay_mtu
}
# =====================================================================
# D-114 -- Office1 site containment VM + its base image.
#
# voffice1 IS the Office1 "facility": MAAS-region runs on it, LXD runs on it, and the
# non-stack service machines (NetBox, GitBucket, Tailscale) are VMs MAAS COMPOSES into
# that LXD host -- VR0's proven `lxd` + `tailscale` pattern, applied per site.
#
# expose_nested_virt = true is LOAD-BEARING, not a nicety: LXD virtual machines are
# qemu/KVM guests, so without the host `svm` feature passed through NO service VM can
# ever boot inside voffice1 and D-114's model fails at its first step. (cloudinit-vm had
# NO cpu block at all before 2026-07-13, which silently gave every guest an emulated CPU
# with no svm -- measured: host is an EPYC 9965, a default-CPU guest sees "Opteron_G3".)
#
# Only network is office1-local: voffice1 reaches the internet THROUGH the OPNsense edge,
# like a real server behind a real site router. It takes its address by DHCP from the
# edge's Kea -- which also makes it the FIRST REAL LEASE that path has ever served.
#
# Cloud-init here is DELIBERATELY MINIMAL (identity + access + guest agent). MAAS and LXD
# are installed as separate GATED steps so they are observable and individually approved,
# not buried in a first-boot script that either silently works or silently does not.
# =====================================================================
module "ubuntu_noble_base" {
source = "./modules/base-image"
image_name = "ubuntu-24.04-base.qcow2"
pool_name = module.office1_storage.pool_name
# Official Ubuntu 24.04 LTS (noble) cloud image. VERIFIED reachable 2026-07-13 (HTTP 200,
# Last-Modified 2026-07-05). 24.04 matches what VR0 deploys for its lxd/juju/tailscale
# machines, so the image choice carries no new delta.
source_url = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
}
module "voffice1" {
source = "./modules/cloudinit-vm"
vm_name = "voffice1"
vcpu = var.voffice1_vcpu
memory_mib = var.voffice1_memory_mib
disk_size_bytes = var.voffice1_disk_bytes
pool_name = module.office1_storage.pool_name
base_volume_path = module.ubuntu_noble_base.path
network_names = [module.office1_network.network_name]
expose_nested_virt = true
user_data = <<-EOT
#cloud-config
hostname: voffice1
fqdn: voffice1.${var.domain_suffix}
manage_etc_hosts: true
users:
- name: jessea123
groups: [adm, sudo]
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
- ${trimspace(file(var.office1_ssh_pubkey_path))}
package_update: true
packages:
- qemu-guest-agent
runcmd:
- [systemctl, enable, --now, qemu-guest-agent]
EOT
meta_data = <<-EOT
instance-id: voffice1-d114
local-hostname: voffice1
EOT
# Match by GLOB, not a guessed interface name: the NIC's kernel name (ens3 / enp1s0 /
# ...) depends on the machine type and PCI topology and is NOT known until first boot.
# Naming it would be an inferred value. dhcp4 is a DELIBERATE choice here, not the
# module's refused default: office1-local genuinely has an authoritative DHCP server
# (Kea on the OPNsense edge, pool .100-.199). A Kea RESERVATION is added afterwards,
# via the OPNsense REST API, to make the address stable.
network_config = <<-EOT
version: 2
ethernets:
lan:
match:
name: "en*"
dhcp4: true
EOT
}