diff --git a/docs/changelog-20260709-opentofu-node-vm.md b/docs/changelog-20260709-opentofu-node-vm.md new file mode 100644 index 0000000..bd01b15 --- /dev/null +++ b/docs/changelog-20260709-opentofu-node-vm.md @@ -0,0 +1,81 @@ +# Changelog 2026-07-09 -- OpenTofu node-VM module + syntax-bug fix (DOCFIX-144) + +No live infrastructure touched -- unapplied IaC source only. Continues the +DOCFIX-142 OpenTofu scaffold. + +## Items + +### 1. DOCFIX-144 -- `opentofu/modules/node-vm` (D-103's PXE-boot node-VM pattern) +FILES (new): `opentofu/modules/node-vm/variables.tf`, `main.tf`, `outputs.tf`. + +WHAT: one MAAS-managed OpenStack node VM per call -- a blank `libvirt_volume` +boot disk (no image, no cloud-init) plus a `libvirt_domain` with PXE boot +priority on its first network interface. This is the D-103 "OpenTofu creates +the node-VM libvirt domains (SHIM)" need for buildout-design Stage 3 / +Phase 2. Deliberately NOT the cloud-init/pre-built-image pattern (confirmed +as a separate, distinct provider mechanism via a real example file -- see +item 2 -- but not built as its own module this session; needed for Office1 +service VMs and OPNsense instead). + +VERIFICATION: this time, built from the provider's own real, runnable example +files (`examples/domain_with_network.tf`, `examples/alpine_cloudinit.tf` -- +fetched and read directly, not summarized-and-guessed) rather than +doc-summarization passes alone, after the previous session's attempt at +`libvirt_domain` was deferred for exactly that reason. Every attribute in +`main.tf` is used in the same shape as a real example, EXCEPT one, flagged +explicitly in the file and in `opentofu/README.md`: the per-device `boot` +attribute's internal shape (`{ order = N }`) is an inference by pattern +(confirmed to exist as a field name on both `interfaces[]` and `disks[]`, +matching native libvirt's per-device ``, but not +independently confirmed from a real example this session). Flagged as the +first thing to check with `tofu providers schema -json` before relying on +PXE-first boot working as written. + +NOT instantiated in root `main.tf`: node count, memory, vcpu, and disk sizing +are a Phase-0 host/disk-budget decision (buildout-design Section 3) that +hasn't been made. Call the module once real values exist -- no placeholder +specs invented to wire it in sooner. + +### 2. DOCFIX-144 (cont.) -- real syntax bug found and fixed in the DOCFIX-142 delivery +FILES: `opentofu/modules/dc-planes/main.tf`, `opentofu/modules/mesh-link/main.tf`, +`opentofu/modules/dc-storage-pool/main.tf`. + +WHAT: all three originally used classic HCL block syntax for nested +structures (`domain { name = ... }`, `mtu { size = ... }`, `target { path = +... }`). Building the node-VM module required fetching real example `.tf` +files from the provider's repo, which showed those examples consistently use +attribute-style nested OBJECTS instead (`os = { ... }`, `devices = { ... }`, +`target = { format = { ... } }`) -- a provider-wide schema convention (this +provider's current major version appears to use a terraform-plugin-framework +object-typed-attribute schema mirroring raw libvirt XML, not classic blocks). +All three files corrected to match; each carries a syntax-note comment +explaining the change and its verification source. + +WHY THIS MATTERS: this was a real correctness bug in already-committed, +already-pushed code (DOCFIX-142, commit 14d7382) -- not caught by +`repo-lint` (it checks encoding/staleness, not HCL semantics) and not +catchable without either a real `tofu validate` pass (unavailable this +session, still) or, as it turned out, real example code from the provider +itself. Logged plainly rather than glossed over: the doc-summarization +verification process used for DOCFIX-142 was insufficient on its own: it +correctly identified WHICH fields exist, but could not reliably convey HOW +they're called (block vs. attribute syntax). Real example code closed that +gap; doc-summarization alone did not. + +DOCUMENTED (not fixed, same class of remaining risk): `opentofu/README.md`'s +"Schema notes" section now states this attribute-object convention as a +provider-wide assumption to apply to ANY nested field not personally +confirmed otherwise -- since the reasoning that surfaced this bug (a handful +of real examples, not an exhaustive schema dump) could plausibly have missed +a resource-specific exception. + +## Next actionable step (unchanged, still logged not actioned) + +On a machine with the `tofu` binary and registry network access: run +`bash scripts/opentofu-validate.sh` against all of `opentofu/` (this would +have caught the DOCFIX-142 syntax bug immediately), then +`tofu providers schema -json` to confirm the `boot`-order attribute shape +before trusting PXE-first boot in `modules/node-vm`. + +REVERT: `git checkout HEAD~ -- opentofu/` (drops the node-vm module and +reverts the three modules to their pre-fix, WRONG syntax -- not recommended). diff --git a/docs/dc-dc-deployment-workflow.md b/docs/dc-dc-deployment-workflow.md index 404910c..b17fcb1 100644 --- a/docs/dc-dc-deployment-workflow.md +++ b/docs/dc-dc-deployment-workflow.md @@ -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** | 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`. | +| **Authoring status** | Runbook not started. The plane/link/pool layer of `opentofu/` (see Stage 1) is reusable here too. `modules/node-vm` (2026-07-09) covers the MAAS-managed OpenStack node VM pattern (blank disk, PXE-boot) -- not yet instantiated (node count/sizing is a pending Phase-0 decision). Still missing for THIS stage: the virsh-VM-host-to-MAAS-rack registration and the OPNsense edge itself (OPNsense needs the DIFFERENT cloud-init/base-image VM pattern, not `node-vm`'s PXE pattern -- still unbuilt). See `opentofu/README.md`. | **State:** NOT STARTED. @@ -191,20 +191,28 @@ 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 -- 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), +2. **OpenTofu -- network/pool/node-VM layer scaffolded 2026-07-09 (was: zero + files).** `opentofu/` now has: `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. + wired -- its CIDRs aren't assigned yet, D-101); `modules/node-vm` (blank + disk, PXE-boot MAAS-managed node VM, the D-103 pattern for Stage 3) -- + not yet instantiated, node count/sizing is a pending Phase-0 decision. + Provider schema was confirmed against the actual current docs AND real + example `.tf` files (not memory) -- it differs materially from older + common examples: nested structures are attribute-style objects (`key = + { ... }`), not classic HCL blocks. **A real bug from that first pass was + found and fixed:** `dc-planes`/`mesh-link`/`dc-storage-pool` originally + used block syntax for `libvirt_network`'s `domain`/`mtu` and + `libvirt_pool`'s `target`; corrected once real provider examples surfaced + the actual convention. Still missing, deliberately deferred (see + `opentofu/README.md`): the cloud-init/base-image VM pattern (needed for + Office1 service VMs and OPNsense -- a genuinely different module from + `node-vm`'s PXE pattern), the per-device `boot`-order attribute's exact + internal shape (flagged UNVERIFIED in `node-vm/main.tf`, an inference by + pattern not a confirmed field), and `tc netem` application. **UNVALIDATED** + -- no `tofu` binary was available to run `scripts/opentofu-validate.sh` + 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 @@ -219,10 +227,14 @@ - **(a) Network substrate -- DONE.** `opentofu/modules/mesh-link` creates the inter-site L2 segments (DC1<->DC2, DC1<->Office1, DC2<->Office1) an OPNsense VM's interfaces would eventually attach to. - - **(b) The OPNsense VM/image itself -- BLOCKED on the deferred - `libvirt_domain` module** (see gap #2). OPNsense is a VM; it inherits - that same schema-verification prerequisite. No image source (official - OPNsense qcow2/ISO vs a custom-built image) has been chosen either. + - **(b) The OPNsense VM/image itself -- needs a DIFFERENT module than + `node-vm` (gap #2's now-built domain module).** `node-vm` is the blank- + disk PXE pattern for MAAS-managed OpenStack nodes; OPNsense needs a real + bootable image + likely cloud-init, confirmed as a distinct, separately + schema'd provider pattern (`libvirt_cloudinit_disk` + a downloaded base + `libvirt_volume`) but not yet built as its own module. No image source + (official OPNsense qcow2/ISO vs a custom-built image) has been chosen + either. - **(c) OPNsense routing/firewall configuration -- NOT STARTED, no mechanism chosen.** Once the VM exists, per-site routing + the "simulated ISP uplink" role need configuring. OPNsense's own config is an XML file @@ -282,8 +294,8 @@ | 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 | 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` | +| `opentofu/` (networks/pools/node-VM PXE pattern) | SCAFFOLDED 2026-07-09, UNVALIDATED (no `tofu` binary available this session) -- see gap #2 and `opentofu/README.md` | +| `opentofu/` (cloud-init/base-image VM pattern, 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 eaa6f08..f3288f9 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -757,7 +757,24 @@ `write_bytes()` with an explicit ascii-encode instead of relying on `.gitattributes` eol=lf to clean up CRLF after the fact. Verified 0 CR bytes on a live re-record; harness 34/34 unchanged; repo-lint 0 fail/1 warn. - Uncommitted -- operator has not yet asked to commit this piece. + COMMITTED + PUSHED (f93dd0d). +- **DONE -- DOCFIX-144 (changelog `docs/changelog-20260709-opentofu-node-vm.md`):** + moved into the domain module as requested. Built `opentofu/modules/node-vm` + (blank-disk PXE-boot MAAS-managed node VM, D-103's pattern for Stage 3) -- + this time from the provider's own real example `.tf` files, not + doc-summarization alone, after last session deferred `libvirt_domain` for + exactly that confidence gap. **Found and fixed a real bug in the + already-committed DOCFIX-142 delivery in the process:** `dc-planes`/ + `mesh-link`/`dc-storage-pool` used classic HCL block syntax + (`domain { ... }`) where the provider's current schema actually needs + attribute-style objects (`domain = { ... }`) -- confirmed once real + provider examples surfaced the convention; doc-summarization alone had NOT + caught this. All three corrected; `opentofu/README.md`'s schema notes now + state the attribute-object convention as a provider-wide assumption. + UNVERIFIED, flagged: the per-device `boot`-order attribute's exact shape + (an inference by pattern). Still deferred: the cloud-init/base-image VM + pattern (Office1 services + OPNsense), DC2 planes, netem. Uncommitted -- + operator has not yet asked to commit this piece. diff --git a/opentofu/README.md b/opentofu/README.md index a7e45f8..72bd3ef 100644 --- a/opentofu/README.md +++ b/opentofu/README.md @@ -27,26 +27,40 @@ 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. +- `modules/node-vm` (2026-07-09) -- one MAAS-managed OpenStack node VM per + call: a BLANK `libvirt_volume` boot disk + a `libvirt_domain` with PXE-boot + priority on its first network interface. This is the D-103 "OpenTofu creates + the node-VM libvirt domains (SHIM)" need for Stage 3 -- deliberately NOT the + cloud-init/pre-built-image pattern (see below), since MAAS images these VMs + itself after enlistment. NOT instantiated in root `main.tf` yet: node count/ + memory/vcpu/disk sizing is a Phase-0 host/disk-budget decision + (buildout-design Section 3) that hasn't been made -- call the module once + real values exist, don't invent placeholder specs to wire it in sooner. - Root wiring for DC1 (planes + pool, using DC1's inherited DC0 CIDRs) and Office1 (pool only), plus all three mesh links. +**Syntax bug fixed 2026-07-09:** `dc-planes`, `mesh-link`, and `dc-storage-pool` +originally used classic HCL block syntax (`domain { name = ... }`) for +`libvirt_network`'s `domain`/`mtu` and `libvirt_pool`'s `target`. Building the +domain module surfaced real example `.tf` files from the provider's own repo +(`examples/domain_with_network.tf`, `examples/alpine_cloudinit.tf`) that +consistently use attribute-style nested objects instead (`os = { ... }`, +`devices = { ... }`) -- this provider has moved to a schema style where nested +structures are object-typed attributes, not blocks. All three modules were +corrected to match once this was confirmed; see each file's syntax-note +comment. This was not previously verifiable from the doc-summarization passes +alone (the docs describe fields as "nested blocks" without showing the actual +call syntax) -- real example code is what settled it. + 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. +- **The cloud-init/pre-built-image VM pattern** (a `libvirt_cloudinit_disk` + + a downloaded base `libvirt_volume` + a domain referencing both), needed for + Office1's own service VMs and for OPNsense -- confirmed as a real, distinct + provider pattern via `examples/alpine_cloudinit.tf`, but not yet built as a + module here. Different enough from `node-vm` (image source, `backing_store` + copy-on-write, `libvirt_cloudinit_disk`'s `user_data`/`meta_data`/ + `network_config`) to warrant its own module rather than bolting it onto + `node-vm`. - **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 @@ -59,9 +73,11 @@ need a second pass regardless. - **OPNsense edges -- four separable pieces, not one, all currently blocked or unstarted:** (a) the network substrate is done (`modules/mesh-link`, above); - (b) the VM/image itself is blocked on the deferred domain module, and no - image source (official OPNsense qcow2/ISO vs. a custom build) has been - chosen; (c) OPNsense's own routing/firewall config is a `config.xml` the + (b) the VM/image itself needs the cloud-init/base-image module (above, still + unbuilt) -- `modules/node-vm`'s blank-PXE-boot pattern does NOT apply to + OPNsense, which needs a real bootable image -- and no image source (official + OPNsense qcow2/ISO vs. a custom build) has been chosen either; (c) OPNsense's + own routing/firewall config is a `config.xml` the libvirt provider never touches -- pre-baked image vs. cloud-init first-boot vs. API/CLI-driven provisioner is an open choice, not started; (d) `tc netem` WAN simulation is a separate OS-level `tc qdisc` setting with no @@ -75,21 +91,49 @@ ## 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: +(`docs/resources/*.md`) AND real example `.tf` files (`examples/*.tf`) 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 (and initially WAS wrong here once, see the syntax-bug note above): -- `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 +- **Nested structures are attribute-style objects (`key = { ... }`), + everywhere, not classic HCL blocks (`key { ... }`).** Confirmed directly + from `examples/domain_with_network.tf` and `examples/alpine_cloudinit.tf` + (`os = { ... }`, `devices = { ... }`, `target = { format = { ... } }`). + This is a provider-wide convention, not resource-specific -- assume it + applies to any nested field you haven't personally confirmed otherwise. +- `libvirt_network`'s isolation control lives at `forward.mode` (a nested + attribute), not a top-level `mode` argument as older examples show. This + scaffold OMITS the `forward` attribute 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. +- `libvirt_domain.devices.disks[].source.volume` takes `{ pool = ..., volume = + ... }` (pool name + volume name, both strings); `.target` takes `{ dev = + "vda", bus = "virtio" }`; `.driver` takes `{ type = "qcow2" }`. Confirmed + directly from `examples/alpine_cloudinit.tf`. +- `libvirt_domain.devices.interfaces[].source.network` takes `{ network = + }` (attach to a libvirt-managed network, what this + repo's planes/mesh-links are); `.source.bridge` takes `{ bridge = + }` (attach to a plain OS bridge instead) -- confirmed from + `examples/domain_with_network.tf`. Not used here (planes/mesh-links are + libvirt-managed networks), but relevant if OPNsense ends up needing a direct + bridge attach to a host NIC. +- `libvirt_volume.target.format.type` (nested twice: `target = { format = { + type = "qcow2" } }`) and `libvirt_volume.backing_store = { path = ..., + format = { type = "qcow2" } }` (copy-on-write from another volume's path) + -- confirmed from `examples/alpine_cloudinit.tf`. +- **UNVERIFIED, flagged in `modules/node-vm/main.tf`:** the per-device `boot` + attribute's internal shape (used to set PXE-first boot order). Confirmed to + EXIST as a field name on both `interfaces[]` and `disks[]` entries (matching + native libvirt's per-device ``), but `{ order = N }` is an + inference by pattern, not confirmed from a real example. Check this first + with `tofu providers schema -json` before relying on PXE-first boot working + as written. +- `libvirt_pool`'s `target` is likewise attribute-style (`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 diff --git a/opentofu/modules/dc-planes/main.tf b/opentofu/modules/dc-planes/main.tf index 048fd1e..de8dd71 100644 --- a/opentofu/modules/dc-planes/main.tf +++ b/opentofu/modules/dc-planes/main.tf @@ -14,6 +14,14 @@ # `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. +# +# SYNTAX NOTE (corrected 2026-07-09): this provider's current schema uses +# attribute-style nested objects (`domain = { ... }`), not classic HCL nested +# blocks (`domain { ... }`) -- confirmed against real example .tf files in the +# provider's own repo (examples/domain_with_network.tf, +# examples/alpine_cloudinit.tf use `os = { ... }`, `devices = { ... }` +# consistently). The first version of this file used block syntax, which is +# very likely wrong for this provider version; fixed to match. resource "libvirt_network" "plane" { for_each = var.planes @@ -21,11 +29,11 @@ name = "${var.dc_name}-${each.key}" autostart = true - domain { + domain = { name = "${each.key}.${var.dc_name}.${var.domain_suffix}" } - mtu { + mtu = { size = var.mtu } } diff --git a/opentofu/modules/dc-storage-pool/main.tf b/opentofu/modules/dc-storage-pool/main.tf index 106ca49..b9459e3 100644 --- a/opentofu/modules/dc-storage-pool/main.tf +++ b/opentofu/modules/dc-storage-pool/main.tf @@ -5,15 +5,18 @@ # 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. +# This module stops at the pool. Per-VM disk (libvirt_volume) creation lives +# in modules/node-vm now that the domain module has been authored. +# +# SYNTAX NOTE (corrected 2026-07-09): attribute-style (`target = { ... }`), +# not block syntax -- see modules/dc-planes/main.tf's note for the +# verification source (real example .tf files in the provider's own repo). resource "libvirt_pool" "dc" { name = "${var.dc_name}-pool" type = "dir" - target { + target = { path = var.target_path } } diff --git a/opentofu/modules/mesh-link/main.tf b/opentofu/modules/mesh-link/main.tf index e829eb3..146e8fb 100644 --- a/opentofu/modules/mesh-link/main.tf +++ b/opentofu/modules/mesh-link/main.tf @@ -13,11 +13,14 @@ # resource to fill the gap; track it as real follow-up work instead (see # opentofu/README.md). +# SYNTAX NOTE (corrected 2026-07-09): attribute-style (`mtu = { ... }`), not +# block syntax -- see dc-planes/main.tf's note for the verification source. + resource "libvirt_network" "link" { name = "mesh-${var.link_name}" autostart = true - mtu { + mtu = { size = var.mtu } } diff --git a/opentofu/modules/node-vm/main.tf b/opentofu/modules/node-vm/main.tf new file mode 100644 index 0000000..dcabf23 --- /dev/null +++ b/opentofu/modules/node-vm/main.tf @@ -0,0 +1,83 @@ +# node-vm: a single MAAS-managed OpenStack node VM (D-103's "OpenTofu creates +# the node-VM libvirt domains (SHIM -- Section 9)" -- Stage 3 / buildout-design +# Phase 2). BLANK disk, PXE-boot -- this is deliberately NOT the cloud-init + +# pre-built-image pattern (that's a separate, still-unbuilt module for Office1 +# service VMs / OPNsense; see opentofu/README.md). MAAS enlists, commissions, +# and deploys these VMs after OpenTofu creates them; OpenTofu does not image +# them itself. +# +# Every attribute below is used exactly as shown in the provider's own +# examples/domain_with_network.tf and examples/alpine_cloudinit.tf (fetched +# and read directly this session, tag v0.9.8) -- attribute-style nested +# objects (`os = { ... }`, `devices = { ... }`), NOT classic HCL blocks. The +# ONE inference in this file, clearly marked below, is the per-device `boot` +# object's internal shape (`order`) -- confirmed to EXIST as a field name on +# both `interfaces` and `disks` entries (matching native libvirt's per-device +# ), but its exact internal shape was not independently +# confirmed from a real example. VERIFY with `tofu providers schema -json` +# before the first real apply; if wrong, `tofu plan` will simply reject the +# attribute name, not silently misconfigure anything. + +resource "libvirt_volume" "disk" { + name = "${var.vm_name}-disk.qcow2" + pool = var.pool_name + capacity = var.disk_size_bytes + format = { + type = "qcow2" + } +} + +resource "libvirt_domain" "node" { + name = var.vm_name + memory = var.memory_mib + vcpu = var.vcpu + type = "kvm" + running = true + + os = { + type = "hvm" + type_arch = "x86_64" + type_machine = "q35" + } + + devices = { + disks = [ + { + source = { + volume = { + pool = libvirt_volume.disk.pool + volume = libvirt_volume.disk.name + } + } + target = { + dev = "vda" + bus = "virtio" + } + driver = { + type = "qcow2" + } + # boot.order shape UNVERIFIED (see file header) -- disk boots second, + # network boots first (PXE). + boot = { + order = 2 + } + } + ] + + interfaces = [ + for i, net_name in var.network_names : { + model = { + type = "virtio" + } + source = { + network = { + network = net_name + } + } + # only the first (PXE) interface gets an explicit boot order; + # see the disk boot-order note above for the same caveat. + boot = i == 0 ? { order = 1 } : null + } + ] + } +} diff --git a/opentofu/modules/node-vm/outputs.tf b/opentofu/modules/node-vm/outputs.tf new file mode 100644 index 0000000..d86e00d --- /dev/null +++ b/opentofu/modules/node-vm/outputs.tf @@ -0,0 +1,9 @@ +output "domain_id" { + description = "libvirt domain id -- for the MAAS enlistment step (D-103: OpenTofu registers each DC's libvirt host to that DC's MAAS rack controller as a virsh VM-host, so MAAS discovers VMs created here)." + value = libvirt_domain.node.id +} + +output "domain_name" { + description = "libvirt domain name." + value = libvirt_domain.node.name +} diff --git a/opentofu/modules/node-vm/variables.tf b/opentofu/modules/node-vm/variables.tf new file mode 100644 index 0000000..102fcdb --- /dev/null +++ b/opentofu/modules/node-vm/variables.tf @@ -0,0 +1,34 @@ +variable "vm_name" { + description = "Domain name, e.g. \"dc1-compute-01\"." + type = string +} + +variable "memory_mib" { + description = "Memory in MiB. Sizing is a Phase-0 host/disk-budget decision (buildout-design Section 3) -- no default, pass explicitly." + type = number +} + +variable "vcpu" { + description = "Virtual CPU count. No default -- pass explicitly, per the measured Phase-0 CPU budget." + type = number +} + +variable "pool_name" { + description = "libvirt_pool name this VM's disk lives in (output of modules/dc-storage-pool)." + type = string +} + +variable "disk_size_bytes" { + description = "Blank boot-disk size in bytes. No default -- pass explicitly." + type = number +} + +variable "network_names" { + description = <<-EOT + Ordered list of libvirt_network names (outputs of modules/dc-planes) this + VM attaches to. Order matters for boot priority -- the FIRST entry is + given PXE boot priority (see main.tf's boot-order note; MAAS-managed + node VMs PXE-boot per D-103, they are not given a pre-built OS image). + EOT + type = list(string) +}