diff --git a/docs/changelog-20260709-opentofu-scaffold.md b/docs/changelog-20260709-opentofu-scaffold.md new file mode 100644 index 0000000..7f550b9 --- /dev/null +++ b/docs/changelog-20260709-opentofu-scaffold.md @@ -0,0 +1,86 @@ +# Changelog 2026-07-09 -- OpenTofu network/pool scaffold (DOCFIX-142) + +First OpenTofu content in the repo, addressing the largest item in the +tooling gap register (`docs/dc-dc-deployment-workflow.md` gap #2). Executed +under blanket approval; no live infrastructure touched -- this is unapplied +IaC source only, and no live cloud/vcloud-host mutation occurred or was +attempted. Ratification note: D-100..D-110 (which this code encodes the +mechanics of) remain ALL PROPOSED (Stage 0 of the workflow doc); nothing here +bakes in a specific unratified value as fact -- CIDRs/MTU/paths are either +required inputs with no default, or DC1's already-inherited-from-DC0 values +(D-101), never an invented DC2 or netem specific. + +## Items + +### 1. DOCFIX-142 -- `opentofu/` network + storage-pool scaffold +FILES (new): `opentofu/versions.tf`, `opentofu/variables.tf`, `opentofu/main.tf`, +`opentofu/README.md`, `opentofu/modules/dc-planes/*`, +`opentofu/modules/mesh-link/*`, `opentofu/modules/dc-storage-pool/*`. + +WHAT: the six per-DC plane segments (D-052/D-100) and the D-100 dark-fiber +mesh triangle (DC1<->DC2, DC1<->Office1, DC2<->Office1), each as an isolated +`libvirt_network` resource (no `forward`/`ips`/`dhcp` block -- NetBox is the +IPAM apex per skill hard rule 3, not libvirt DHCP); plus a directory-backed +`libvirt_pool` per DC/Office1 for future node-VM disks. Root module wires DC1 +(using its DC0-inherited CIDRs, D-101) and Office1; DC2 is written but +commented out pending its NetBox CIDR assignment (D-101 open sub-item). + +WHY: `opentofu/` had zero files before this -- the single largest item in the +2026-07-09 tooling gap register, and a hard blocker for buildout-design +Stage 1-2 (vcloud host prep, Office1 headend, per-DC substrate creation). + +SCHEMA VERIFICATION (read `opentofu/README.md` for the full account): the +`dmacvicar/libvirt` provider's current schema (confirmed against its actual +`docs/resources/*.md`, tag v0.9.8, fetched this session -- NOT taken from +training-data memory of older provider versions, which materially differs) +turned out to nest network isolation under `forward.mode` rather than a +top-level `mode` argument, and to have restructured `libvirt_domain` into a +~40-argument schema mirroring raw libvirt XML. The `forward.mode` accepted +value enum could not be confirmed from the reachable docs this session; this +scaffold sidesteps the ambiguity by omitting the `forward` block entirely +(documented native-libvirt isolated-network behavior) rather than guessing a +value. Flagged prominently in `opentofu/README.md` as the first thing to +re-check if `tofu validate` objects. + +DELIBERATELY NOT BUILT, and why (see `opentofu/README.md` "Scope of this +delivery" for the full list): `libvirt_domain` (node VMs) and `libvirt_volume` +(disk images) -- the domain schema's nested `devices` block (disks, +interfaces, filesystems, graphics, etc.) could not be verified with enough +confidence this session to author safely; DC2's plane networks -- CIDRs not +yet assigned; `tc netem` WAN-simulation application -- an OS-level `tc qdisc` +setting with no equivalent `libvirt_network` argument, needing a provisioner +or companion script not yet written, and whose parameters are themselves a +Phase-0 open item (D-100); OPNsense/MAAS-region/NetBox/GitBucket service VMs +-- all depend on the deferred domain module. + +HARNESS: `scripts/opentofu-validate.sh` (fmt-check + init + validate wrapper, +BASH_SOURCE-self-locating, no hardcoded repo name) + `tests/opentofu-validate/` +(2/2 green -- only the guard-clause paths this environment can exercise +without a `tofu` binary; the fmt/init/validate path itself is UNTESTED here, +logged not hidden). Also extended `scripts/repo_lint.py`'s L1 (ASCII/LF) file +extension set to include `.tf` (it did not cover Terraform/OpenTofu files at +all before this, since none existed) -- verified this introduces no new +findings (the new `.tf` files are plain ASCII/LF) and the 34/34 repo-lint +harness still passes. + +REVERT: `git rm -r opentofu/ scripts/opentofu-validate.sh tests/opentofu-validate/`; +revert the one-line `exts` change in `scripts/repo_lint.py` and its harness +counterpart if reverting fully (not recommended -- `.tf` coverage is a real +gap otherwise, independent of whether this specific scaffold survives). + +### 2. Workflow doc + visual tracker updated to match +FILES: `docs/dc-dc-deployment-workflow.md`, the companion Artifact (same URL, +redeployed) -- Stage 1/3 authoring-status notes, gap register item 2, and the +status summary table all updated to reflect the scaffold's existence and its +UNVALIDATED state, per the "update this as stages close" standing instruction +on that doc. + +REVERT: `git checkout HEAD~ -- docs/dc-dc-deployment-workflow.md`. + +## Next actionable step (logged, not actioned) + +On a machine with the `tofu` binary and registry network access: run +`bash scripts/opentofu-validate.sh`, then `tofu providers schema -json` against +the initialized provider, and read the actual `libvirt_domain`/`libvirt_volume` +schema directly before authoring the node-VM module -- do not proceed from +memory or from a doc-summarization pass a second time. diff --git a/docs/dc-dc-deployment-workflow.md b/docs/dc-dc-deployment-workflow.md index 6e89cd2..59ea2cd 100644 --- a/docs/dc-dc-deployment-workflow.md +++ b/docs/dc-dc-deployment-workflow.md @@ -49,7 +49,7 @@ | **Gate** | Nested KVM verified; disk budget computed + Ceph size decision recorded; measured MTU recorded; virtual networks present and isolated as designed. | | **Owns** | D-100 (fabric), D-101/D-102 (MTU), buildout-design Section 3 (host/disk/MTU gates). | | **Reuse vs new** | NEW. No VR0-DC0 analog -- that testcloud IS the single vcloud host, already prepared; this is preparing the host to carry TWO independent DC substrates plus Office1. | -| **Authoring status** | No runbook yet. First `phase-0-dc-dc-*.md`-class runbook to write. | +| **Authoring status** | Runbook not started. `opentofu/` SCAFFOLD started 2026-07-09: `modules/dc-planes` (six per-DC planes) + `modules/mesh-link` (dark-fiber legs) + `modules/dc-storage-pool` wired for DC1 + Office1 -- see `opentofu/README.md` for what's built vs deliberately deferred (node-VM/domain resources, DC2 CIDRs, netem). UNVALIDATED: no `tofu` binary available to run `scripts/opentofu-validate.sh` yet. | **State:** NOT STARTED. @@ -79,7 +79,7 @@ | **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). | | **Reuse vs new** | NEW (no OpenTofu/multi-rack precedent in VR0 DC0). Explicitly tagged shim: node-VM creation has no Roosevelt analog (physical racking + BMC enlistment replaces it there) -- do not treat as reusable production IaC. | -| **Authoring status** | No runbook/OpenTofu module yet. | +| **Authoring status** | Runbook not started. The plane/link/pool layer of `opentofu/` (see Stage 1) is reusable here too -- what's still missing for THIS stage specifically is the node-VM (`libvirt_domain`) module, the virsh-VM-host-to-MAAS-rack registration, and the OPNsense edge itself; all deferred, see `opentofu/README.md`. | **State:** NOT STARTED. @@ -191,10 +191,20 @@ a `$DC` (or `--dc`) selector, with both libraries becoming DC-keyed lookups instead of flat constants. Do this BEFORE writing the Stage 5 runbook, or the "high reuse" claim above is not actually true operationally. -2. **OpenTofu -- zero files exist.** No `.tf`/`.tofu` anywhere in the repo. Blocks - Stage 1-2 entirely: libvirt domains, the dark-fiber mesh + plane segments, - per-site OPNsense edges, and the Office1 service VMs (MAAS-region, NetBox, - GitBucket) all depend on modules that don't exist yet, not even a skeleton. +2. **OpenTofu -- PARTIALLY SCAFFOLDED 2026-07-09 (was: zero files).** `opentofu/` + now has the network + storage-pool layer: `modules/dc-planes` (six per-DC + planes), `modules/mesh-link` (the D-100 dark-fiber triangle legs), + `modules/dc-storage-pool`, wired for DC1 + Office1 (DC2 deliberately not + wired -- its CIDRs aren't assigned yet, D-101). Provider schema (confirmed + against the actual current docs, not memory) turned out to differ + materially from older common examples of this provider. Still missing, + deliberately deferred (see `opentofu/README.md` for why): the `libvirt_domain` + node-VM module (schema too large/unfamiliar to author safely without a + real `tofu providers schema` pass), `libvirt_volume`, OPNsense edge config, + and `tc netem` application. **UNVALIDATED** -- no `tofu` binary was + available to run `scripts/opentofu-validate.sh` (the harness written + alongside it) this session; run it on a connected machine before trusting + any of this. 3. **NetBox multi-DC/dual-stack import pipeline.** `netbox/` has exactly two scripts (`ipv4-prefixes-import.py`, `ipv6-mark-reserved.py`) -- single-DC v4 import and v6-reservation-marking only. D-101 requires DC2's supernet, the @@ -247,7 +257,9 @@ | Design (`dc-dc-buildout-design.md`) | DONE, PROPOSED | | Decisions (D-100..D-110) | DRAFTED, ALL PROPOSED -- **Stage 0 is the very next action** | | DR mechanism seed | DONE (`dc-dc-replication-DR-seed.md`), superseded-carrier corrected into D-108 | -| Stage 0-7 runbooks/OpenTofu modules | NOT YET WRITTEN -- see Tooling gap register above for the itemized breakdown | +| Stage 0-7 runbooks | NOT YET WRITTEN -- see Tooling gap register above for the itemized breakdown | +| `opentofu/` (networks/pools) | SCAFFOLDED 2026-07-09, UNVALIDATED (no `tofu` binary available this session) -- see gap #2 and `opentofu/README.md` | +| `opentofu/` (node VMs, OPNsense, netem) | NOT STARTED -- deliberately deferred, see `opentofu/README.md` | | Reusable as-is, repo-agnostic (2026-07-09 sweep) | `bundle.yaml`, `phase-01..08` VR0 DC0 runbooks (Stage 5/7 template, once DC-parameterized -- gap #1), `preflight.sh`, `cloud-assert.sh`, `repo-lint.sh`, `run-logged.sh`, `ledger-scan.sh` | | `lib-net.sh` / `lib-hosts.sh` | NOT yet DC-agnostic -- single-deployment hardcoded literals; see gap #1 | | Environment/skill docs (`environment.md`, README.md) | Still describe VR0 DC0/v1 single-DC scope wholesale -- flagged, not yet rewritten for VR1; not a blocker for Stage 0 but will need doing before/during Stage 1 | diff --git a/docs/session-ledger.md b/docs/session-ledger.md index c6ca554..9d2392e 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -715,6 +715,36 @@ introduced here, DOCFIX candidate (teach the checks the other conventions, or standardize runbooks onto fences). Uncommitted -- operator has not yet asked to commit. +- **DONE -- workflow doc + gap register (2026-07-09):** `docs/dc-dc-deployment-workflow.md` + added (Stage 0-7 followable checklist + tooling gap register audited + directly against the repo) + a companion visual Artifact + (https://claude.ai/code/artifact/e752f6dd-23ff-4c59-9ce5-1dedb917e5ea). + COMMITTED + PUSHED (374aef5). +- **DONE -- DOCFIX-142 (changelog `docs/changelog-20260709-opentofu-scaffold.md`):** + first OpenTofu content in the repo (gap register item 2). `opentofu/` + network + storage-pool scaffold: `dc-planes` (six per-DC planes), + `mesh-link` (D-100 dark-fiber triangle), `dc-storage-pool` modules; DC1 + + Office1 wired, DC2 held back pending its CIDR assignment (D-101). Provider + schema (`dmacvicar/libvirt` v0.9.8) verified against its actual current docs + this session, not memory -- differs materially from older common examples + (network isolation is `forward.mode`, nested, not top-level `mode`; domain + resource restructured to ~40 args mirroring raw libvirt XML). Node-VM/volume + resources, OPNsense, DC2 planes, and `tc netem` application DELIBERATELY + DEFERRED -- schema too unfamiliar to author safely without a real + `tofu providers schema` pass; see `opentofu/README.md`. Added + `scripts/opentofu-validate.sh` + `tests/opentofu-validate/` (2/2 green -- + only the guard-clause paths testable without a `tofu` binary, which is not + available anywhere this repo has been worked in this session; the actual + fmt/init/validate path is UNTESTED, logged not hidden). Extended + `repo_lint.py`'s L1 file-extension set to include `.tf` (previously + uncovered since no Terraform/OpenTofu files existed). repo-lint 34/34 + harness green, 0 fail/1 documented warn. Uncommitted -- operator has not yet + asked to commit this piece. +- **NEXT ACTIONABLE STEP (logged):** on a machine with the `tofu` binary + + registry network access, run `scripts/opentofu-validate.sh`, then + `tofu providers schema -json` against the initialized provider, and read the + real `libvirt_domain`/`libvirt_volume` schema directly before authoring the + node-VM module. diff --git a/opentofu/README.md b/opentofu/README.md new file mode 100644 index 0000000..8196e7d --- /dev/null +++ b/opentofu/README.md @@ -0,0 +1,93 @@ +# opentofu/ -- VR1 (DC-DC) infrastructure-as-code + +Owns create/destroy of the "physical" layer per D-103: libvirt domains (node +VMs), all virtual networks (dark-fiber mesh, per-DC planes, ISP-edge), the +OPNsense edge VMs, and the Office1 service VMs. Runs from the Office1 operator +VM against the vcloud host libvirt. See `docs/dc-dc-buildout-design.md` and +`docs/dc-dc-deployment-workflow.md` (Stage 1-2, tooling gap register item 2) +for the surrounding plan. + +**Status: SCAFFOLD, UNVALIDATED.** No `tofu`/`terraform` binary was available +in the session that authored this -- nothing here has been run through +`tofu init`/`validate`/`plan`, let alone `apply`. Before trusting any of it: + +``` +bash scripts/opentofu-validate.sh +``` + +on a machine with the binary and network access to the provider registry, and +read the result before proceeding. + +## Scope of this delivery + +Built: +- `modules/dc-planes` -- the six per-DC plane segments (D-052/D-100) as + isolated `libvirt_network` resources. +- `modules/mesh-link` -- one inter-site L2 segment per leg of the D-100 + dark-fiber triangle (DC1<->DC2, DC1<->Office1, DC2<->Office1). +- `modules/dc-storage-pool` -- a directory-backed `libvirt_pool` per DC/Office1 + for future node-VM disk images. +- Root wiring for DC1 (planes + pool, using DC1's inherited DC0 CIDRs) and + Office1 (pool only), plus all three mesh links. + +Deliberately NOT built yet, and why: +- **`libvirt_domain` (the actual node VMs).** The provider's current schema for + this resource turned out to be a large (~40 top-level argument), deeply + nested rewrite that closely mirrors raw libvirt domain XML (`numa_tune`, + `launch_security`, `memory_backing`, a `devices` block containing `disks`, + `interfaces`, `filesystems`, `graphics`, etc.) -- materially different from + older, commonly-referenced examples of this provider. Doc-summarization + tools available this session could not give high-confidence, verbatim field + names for the nested `devices.interfaces`/`devices.disks` shape specifically. + Authoring this blind risks exactly the "fabricated flag/value" failure mode + this repo's operating discipline calls out as the cardinal sin. **Next + action:** on a machine with the `tofu` binary, run + `tofu providers schema -json` (after `terraform init`) and read the actual + `libvirt_domain` schema directly, or start from the provider's own + `examples/` directory in its GitHub repo, before writing this module. +- **`libvirt_volume` (per-VM disk images).** Follows directly from the domain + module above; deferred with it. +- **DC2's plane networks.** D-101 has not assigned DC2's supernet yet (it's an + explicit open sub-item). `main.tf` has the DC2 module block written and + commented out, ready to uncomment once NetBox assigns real CIDRs -- do not + fill in a guessed default to make it "work" sooner. +- **`tc netem` WAN simulation.** `libvirt_network` has no netem-equivalent + argument; this is an OS-level `tc qdisc` setting applied to the bridge + interface a `mesh-link` module creates, which needs either a `null_resource` + provisioner or a small companion script -- not yet written. The exact + parameters are themselves a Phase-0 open item (D-100), so this was going to + need a second pass regardless. +- **OPNsense edge VM config / MAAS-region / NetBox / GitBucket service VMs.** + All depend on the deferred domain module (they're VMs). + +## Schema notes (read before extending) + +Verified this session against the provider's actual current docs +(`docs/resources/*.md` in `dmacvicar/terraform-provider-libvirt`, tag +`v0.9.8`, fetched 2026-07-09) -- NOT from training-data memory of older +provider versions, which would have been wrong: + +- `libvirt_network`'s isolation control lives at `forward.mode` (a NESTED + block field), not a top-level `mode` argument as older examples show. This + scaffold OMITS the `forward` block entirely for isolated planes/links + (documented native-libvirt behavior for a private, unforwarded switch) + rather than guess a `mode = "none"` value this session could not confirm is + still valid. Re-check this specific choice first if `tofu validate` objects. +- `domain` and `mtu` on `libvirt_network` are nested blocks (`domain { name = + ... }`, `mtu { size = ... }`), not flat string/number arguments. +- `libvirt_pool`'s `target` is a nested block (`target { path = ... }`), types + like `dir` are native libvirt storage-pool vocabulary (stable, long-standing, + not provider-specific) -- reasonably high confidence. + +## Conventions carried from the rest of this repo + +- No hardcoded repo name (repo-lint L9 would need extending to cover `.tf` + files if this becomes a recurring class of finding -- not yet done, flag if + it comes up). +- Every variable without a safe, already-ratified default requires explicit + input (no invented fallbacks) -- same "never use an inferred value" + discipline as everywhere else in this repo. +- DC1's plane CIDRs are a second, hand-maintained copy of + `scripts/lib-net.sh`'s `PLANE_CIDRS`. Keep them in sync manually until a + generator exists to derive one from the other (DOCFIX candidate, not + actioned). diff --git a/opentofu/main.tf b/opentofu/main.tf new file mode 100644 index 0000000..bec3169 --- /dev/null +++ b/opentofu/main.tf @@ -0,0 +1,77 @@ +# 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 +} + +# ---- 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: NOT YET WIRED. ---- +# D-101 has not assigned DC2's supernet yet (open sub-item). Once NetBox +# assigns it, add a `dc2_planes` variable (same shape as `dc1_planes` in +# variables.tf) and uncomment/adapt this block -- do not fill in guessed +# CIDRs to make this "work" sooner. +# +# 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 +} + +# ---- 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 +} diff --git a/opentofu/modules/dc-planes/main.tf b/opentofu/modules/dc-planes/main.tf new file mode 100644 index 0000000..048fd1e --- /dev/null +++ b/opentofu/modules/dc-planes/main.tf @@ -0,0 +1,31 @@ +# dc-planes: the six per-DC plane segments (D-052 template, D-100 independent- +# cloud refinement) as isolated libvirt virtual networks. +# +# Deliberately isolated, not NAT/routed: OpenStack (Neutron/OVN) and NetBox own +# addressing and routing on these segments, not libvirt. The `forward` block is +# OMITTED entirely, which is documented native-libvirt behavior for a private, +# unforwarded virtual switch -- this repo could not confirm the current +# provider's `forward.mode` accepted string values from the published docs +# (the schema field exists; its value enum was not documented in prose form +# reachable this session). Confirm with `tofu providers schema -json` on a +# connected machine before the first real apply; if `mode = "none"` turns out +# to still be required explicitly, add it then -- do not guess it in now. +# +# `ips`/`dhcp` are likewise intentionally NOT set: NetBox is the IPAM apex +# (skill hard rule 3); libvirt-managed DHCP on these planes would create a +# second, conflicting source of truth for addresses OpenStack itself assigns. + +resource "libvirt_network" "plane" { + for_each = var.planes + + name = "${var.dc_name}-${each.key}" + autostart = true + + domain { + name = "${each.key}.${var.dc_name}.${var.domain_suffix}" + } + + mtu { + size = var.mtu + } +} diff --git a/opentofu/modules/dc-planes/outputs.tf b/opentofu/modules/dc-planes/outputs.tf new file mode 100644 index 0000000..b309545 --- /dev/null +++ b/opentofu/modules/dc-planes/outputs.tf @@ -0,0 +1,9 @@ +output "network_ids" { + description = "libvirt network id, keyed by plane name -- for wiring into a future dc-nodes module's network_interface blocks (not yet authored; see opentofu/README.md)." + value = { for k, v in libvirt_network.plane : k => v.id } +} + +output "network_names" { + description = "libvirt network name, keyed by plane name." + value = { for k, v in libvirt_network.plane : k => v.name } +} diff --git a/opentofu/modules/dc-planes/variables.tf b/opentofu/modules/dc-planes/variables.tf new file mode 100644 index 0000000..ae2fd94 --- /dev/null +++ b/opentofu/modules/dc-planes/variables.tf @@ -0,0 +1,31 @@ +variable "dc_name" { + description = "Short DC identifier used in resource naming and DNS (e.g. \"dc1\", \"dc2\", \"office1\"). Matches the D-106 naming convention (.omega..vr1.cloud.neumatrix.local)." + type = string +} + +variable "domain_suffix" { + description = "Base domain the per-plane libvirt DNS domain is built under. Anchor value: cloud.neumatrix.local (per D-106); NOT re-derived here, pass it in explicitly." + type = string +} + +variable "planes" { + description = <<-EOT + Map of plane-name -> plane definition, one entry per D-052/D-100 six-plane + template for this DC. CIDR values must come from NetBox (the IPAM apex, + per D-101) or scripts/lib-net.sh for DC1 (which inherits the DC0 layout + unchanged) -- never invent a CIDR here. DC2's CIDRs are NOT YET ASSIGNED + (D-101 open sub-item); do not guess a default for them. + EOT + type = map(object({ + cidr = string # anchor fact only -- NOT applied to the libvirt_network + # resource (isolated planes carry no forward/ips block, see + # main.tf); recorded here purely so `tofu plan` output and + # `terraform output` reflect the intended assignment for + # cross-checking against NetBox. + })) +} + +variable "mtu" { + description = "Underlay MTU for every plane network in this DC. MUST be a Phase-0 MEASURED value (buildout-design Section 3 / D-102), never assumed jumbo. No default -- pass explicitly." + type = number +} diff --git a/opentofu/modules/dc-storage-pool/main.tf b/opentofu/modules/dc-storage-pool/main.tf new file mode 100644 index 0000000..106ca49 --- /dev/null +++ b/opentofu/modules/dc-storage-pool/main.tf @@ -0,0 +1,19 @@ +# dc-storage-pool: one directory-backed libvirt storage pool per DC, for the +# node-VM disk images OpenTofu will create (buildout-design Phase 0: "libvirt +# storage pools + thin-provisioning"). Deliberately the simplest pool type +# ("dir") -- thin-provisioning here is a property of the underlying host +# filesystem/volume, not a libvirt_pool argument; there is no thin-provisioning +# switch on this resource to set. +# +# This module stops at the pool. It does NOT create libvirt_volume resources +# (per-VM disk images) -- that is deferred alongside the domain/node-VM module; +# see opentofu/README.md for why. + +resource "libvirt_pool" "dc" { + name = "${var.dc_name}-pool" + type = "dir" + + target { + path = var.target_path + } +} diff --git a/opentofu/modules/dc-storage-pool/outputs.tf b/opentofu/modules/dc-storage-pool/outputs.tf new file mode 100644 index 0000000..c40e544 --- /dev/null +++ b/opentofu/modules/dc-storage-pool/outputs.tf @@ -0,0 +1,4 @@ +output "pool_name" { + description = "libvirt pool name -- for wiring into a future libvirt_volume/libvirt_domain module." + value = libvirt_pool.dc.name +} diff --git a/opentofu/modules/dc-storage-pool/variables.tf b/opentofu/modules/dc-storage-pool/variables.tf new file mode 100644 index 0000000..82c3f8a --- /dev/null +++ b/opentofu/modules/dc-storage-pool/variables.tf @@ -0,0 +1,16 @@ +variable "dc_name" { + description = "Short DC identifier used in resource naming (e.g. \"dc1\", \"dc2\", \"office1\")." + type = string +} + +variable "target_path" { + description = <<-EOT + Host filesystem path for this pool's storage (a "dir"-type pool: a plain + directory-backed libvirt storage pool, thin-provisioned by the underlying + filesystem, not by any libvirt-level setting -- confirm the host volume + this path lives on is actually thin-provisioned, per buildout-design + Section 3). MUST be a real, measured path on the vcloud host -- never + invented; no default. + EOT + type = string +} diff --git a/opentofu/modules/mesh-link/main.tf b/opentofu/modules/mesh-link/main.tf new file mode 100644 index 0000000..e829eb3 --- /dev/null +++ b/opentofu/modules/mesh-link/main.tf @@ -0,0 +1,23 @@ +# mesh-link: one dedicated virtual L2 segment standing in for a dark-fiber leg +# of the D-100 triangle (DC1<->DC2, DC1<->Office1, DC2<->Office1 -- mesh, not +# star). Same isolated-network shape as dc-planes/main.tf and for the same +# reason: this is a bare L2 wire, not something libvirt should route or NAT. +# +# WAN SIMULATION (tc netem) IS DELIBERATELY NOT HERE. `libvirt_network` has no +# netem-equivalent argument (confirmed against the current schema) -- netem is +# a Linux traffic-control setting applied to the bridge interface THIS resource +# creates, at the OS level (`tc qdisc add dev ...`), after the network +# exists. That is a separate, not-yet-authored piece (a null_resource with a +# provisioner, or a small companion script) -- exact parameters are themselves +# a Phase-0 open item (D-100). Do not fabricate a netem argument on this +# resource to fill the gap; track it as real follow-up work instead (see +# opentofu/README.md). + +resource "libvirt_network" "link" { + name = "mesh-${var.link_name}" + autostart = true + + mtu { + size = var.mtu + } +} diff --git a/opentofu/modules/mesh-link/outputs.tf b/opentofu/modules/mesh-link/outputs.tf new file mode 100644 index 0000000..91f8bcc --- /dev/null +++ b/opentofu/modules/mesh-link/outputs.tf @@ -0,0 +1,12 @@ +output "network_id" { + description = "libvirt network id for this link -- for wiring into the OPNsense edge VMs' interfaces once the domain/VM module is authored (see opentofu/README.md)." + value = libvirt_network.link.id +} + +# A "bridge_name" output (the OS-level bridge interface backing this network, +# needed by whatever applies `tc netem` to it) is deliberately NOT included +# here: `bridge` is documented as a configurable input block (delay, +# mac_table_manager, name, stp, zone), and this resource does not set it, so +# whether `libvirt_network.link.bridge` reads back a provider-assigned name +# when left unset is unverified from this session. Confirm with +# `tofu show -json` after a real apply before relying on it. diff --git a/opentofu/modules/mesh-link/variables.tf b/opentofu/modules/mesh-link/variables.tf new file mode 100644 index 0000000..3c81a90 --- /dev/null +++ b/opentofu/modules/mesh-link/variables.tf @@ -0,0 +1,9 @@ +variable "link_name" { + description = "Short identifier for this inter-site segment, e.g. \"dc1-dc2\", \"dc1-office1\", \"dc2-office1\" (the D-100 dark-fiber triangle: mesh, not star)." + type = string +} + +variable "mtu" { + description = "Underlay MTU for this link. MUST be a Phase-0 measured value (D-102), never assumed jumbo. No default -- pass explicitly." + type = number +} diff --git a/opentofu/variables.tf b/opentofu/variables.tf new file mode 100644 index 0000000..f9342e2 --- /dev/null +++ b/opentofu/variables.tf @@ -0,0 +1,54 @@ +variable "libvirt_uri" { + description = "libvirt connection URI for the vcloud host (e.g. qemu+ssh://user@host/system). MUST be measured this session, never guessed -- no default." + type = string +} + +variable "domain_suffix" { + description = "Base domain for per-plane libvirt DNS domains. Anchor value per D-106's naming convention (.omega..vr1.cloud.neumatrix.local)." + type = string + default = "cloud.neumatrix.local" +} + +variable "underlay_mtu" { + description = "Underlay MTU applied to every plane and mesh-link network. MUST be the Phase-0 MEASURED host L2 MTU (buildout-design Section 3 / D-102) -- do not assume jumbo (9000). No default -- pass explicitly, e.g. via a Phase-0-specific tfvars file once measured." + type = number +} + +variable "dc1_pool_path" { + description = "Host filesystem path for DC1's libvirt storage pool. Measured, real path on the vcloud host -- no default." + type = string +} + +variable "office1_pool_path" { + description = "Host filesystem path for Office1's libvirt storage pool (MAAS-region/NetBox/GitBucket service-VM disks). Measured, real path on the vcloud host -- no default." + type = string +} + +variable "dc1_planes" { + description = <<-EOT + DC1's six planes. DC1 INHERITS the DC0 v4 layout UNCHANGED (D-101) -- + these CIDRs are copied from scripts/lib-net.sh's PLANE_CIDRS array as it + stood 2026-07-09, not invented here. If lib-net.sh changes, update this + default in the SAME change (it is a second copy of the same fact, not an + independent source -- a future DOCFIX candidate is a generator that keeps + both in sync automatically instead of two hand-maintained copies). + EOT + type = map(object({ + cidr = string + })) + default = { + provider-public = { cidr = "10.12.4.0/22" } + metal-admin = { cidr = "10.12.8.0/22" } + metal-internal = { cidr = "10.12.12.0/22" } + data-tenant = { cidr = "10.12.16.0/22" } + storage = { cidr = "10.12.32.0/22" } + replication = { cidr = "10.12.36.0/22" } + } +} + +# dc2_planes has NO variable/default here yet, deliberately: DC2's supernet is +# a D-101 OPEN SUB-ITEM ("exact prefixes assigned in NetBox, non-colliding with +# DC1..."), not yet assigned anywhere. Do not add a guessed default -- once +# NetBox assigns it, add a dc2_planes variable shaped exactly like dc1_planes +# above and a matching module "dc2_planes" block in main.tf (see the commented +# skeleton there). diff --git a/opentofu/versions.tf b/opentofu/versions.tf new file mode 100644 index 0000000..a962f94 --- /dev/null +++ b/opentofu/versions.tf @@ -0,0 +1,18 @@ +# Provider version CONFIRMED this session (2026-07-09) against the actual +# releases page: dmacvicar/libvirt latest tag is v0.9.8. Pinned to that exact +# release rather than a range -- re-verify and bump deliberately, don't drift. +# (This repo has no OpenTofu/tofu binary available to it this session -- this +# file, and everything under opentofu/, is UNVALIDATED: run +# `bash scripts/opentofu-validate.sh` on a machine with the tofu binary and +# network access to the provider registry before trusting any of it.) + +terraform { + required_version = ">= 1.6.0" + + required_providers { + libvirt = { + source = "dmacvicar/libvirt" + version = "0.9.8" + } + } +} diff --git a/scripts/opentofu-validate.sh b/scripts/opentofu-validate.sh new file mode 100644 index 0000000..63165f7 --- /dev/null +++ b/scripts/opentofu-validate.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# scripts/opentofu-validate.sh [tofu-dir] +# +# Thin syntax/schema gate for opentofu/ (VR1 IaC, D-103). Read-only: runs +# `tofu fmt -check` + `tofu init -backend=false` + `tofu validate` against the +# root module. Does NOT plan or apply -- no provider connection, no libvirt +# reach required. This is the harness opentofu/README.md tells you to run +# before trusting anything under opentofu/, since it was authored in a +# session with no tofu binary available to self-check. +# +# Exit: 0 clean | 1 fmt/init/validate failed | 2 tofu binary not found. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TOFU_DIR="${1:-$HERE/../opentofu}" + +if [ ! -d "$TOFU_DIR" ]; then + echo "FAIL: no such directory: $TOFU_DIR" + exit 1 +fi + +if ! command -v tofu >/dev/null 2>&1; then + echo "FAIL: tofu binary not found on PATH -- install OpenTofu, then re-run this script" + exit 2 +fi + +fail=0 + +echo "== tofu fmt -check -recursive ==" +if ! tofu fmt -check -recursive -diff "$TOFU_DIR"; then + echo " [FAIL] formatting drift -- run: tofu fmt -recursive $TOFU_DIR" + fail=1 +fi + +echo "== tofu init -backend=false (downloads providers; needs registry network access) ==" +if ! ( cd "$TOFU_DIR" && tofu init -backend=false -input=false ); then + echo " [FAIL] init failed" + exit 1 +fi + +echo "== tofu validate ==" +if ! ( cd "$TOFU_DIR" && tofu validate ); then + echo " [FAIL] validate failed" + fail=1 +fi + +if [ "$fail" -eq 0 ]; then + echo "PASS: opentofu-validate ($TOFU_DIR)" + exit 0 +else + echo "FAIL: opentofu-validate ($TOFU_DIR)" + exit 1 +fi diff --git a/scripts/repo_lint.py b/scripts/repo_lint.py index 08f6374..9f1225f 100644 --- a/scripts/repo_lint.py +++ b/scripts/repo_lint.py @@ -168,7 +168,7 @@ return [p for p in out if p.is_file()] def all_text(): - exts = {".md", ".sh", ".py", ".yaml", ".yml", ""} + exts = {".md", ".sh", ".py", ".yaml", ".yml", ".tf", ""} skip_names = {"overrides.zip"} out = [] for p in R.rglob("*"): diff --git a/tests/opentofu-validate/run-tests.sh b/tests/opentofu-validate/run-tests.sh new file mode 100644 index 0000000..7c2586b --- /dev/null +++ b/tests/opentofu-validate/run-tests.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# tests/opentofu-validate/run-tests.sh -- offline harness for +# scripts/opentofu-validate.sh. Only tests the guard clauses this environment +# can actually exercise (no tofu binary is available anywhere this repo has +# been worked in so far -- see opentofu/README.md). The fmt/init/validate path +# itself is UNTESTED here; that is real, logged residual risk, not an +# oversight -- run the script for real on a machine with the tofu binary +# before trusting opentofu/. +# Exit: 0 all pass | 1 any case failed. ASCII + LF. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$HERE/../../scripts/opentofu-validate.sh" +PASS=0; FAIL=0 + +run() { # run