diff --git a/docs/changelog-20260709-opnsense-edge-module.md b/docs/changelog-20260709-opnsense-edge-module.md new file mode 100644 index 0000000..f5cefe6 --- /dev/null +++ b/docs/changelog-20260709-opnsense-edge-module.md @@ -0,0 +1,91 @@ +# Changelog 2026-07-09 -- opnsense-edge module + prep scripts (DOCFIX-146) + +No live infrastructure touched -- unapplied IaC/scripts only. Implements the +mechanism identified in the same-day OPNsense deployment research +(`docs/changelog-20260709-opnsense-research.md`). + +## Items + +### 1. DOCFIX-146 -- `scripts/opnsense-prep-image.sh` +FILE (new): `scripts/opnsense-prep-image.sh` + `tests/opnsense-prep-image/run-tests.sh`. + +WHAT: downloads the OPNsense nano image, decompresses it, converts raw -> +qcow2, and grows it -- the preprocessing `libvirt_volume`'s `create.content.url` +can't do itself (it almost certainly performs a plain fetch, no +decompression). No mirror URL is hardcoded -- `OPNSENSE_MIRROR_BASE` is a +required env var (hard rule 2: never use an inferred value; OPNsense +publishes multiple mirrors and a silently-hardcoded one risks going stale +unnoticed). + +HARNESS: 4/4 green, but only the guard-clause paths this environment can +exercise -- `qemu-img` is genuinely absent here, so that failure path is +real, not synthetic; the actual download/convert/resize behavior is +UNTESTED (would need `qemu-img` + network access to a real mirror). + +### 2. DOCFIX-146 (cont.) -- `scripts/opnsense-build-config-iso.sh` +FILE (new): `scripts/opnsense-build-config-iso.sh` + `tests/opnsense-build-config-iso/run-tests.sh`. + +WHAT: builds a plain ISO9660 image containing `/conf/config.xml`, for +OPNsense's Configuration Importer (see the OPNsense research changelog -- +ISO9660 support was added to the Importer specifically for VM/cloud +automation). Uses `genisoimage`/`xorriso`, whichever is present. + +HARNESS: 3/3 green, same caveat -- neither ISO tool is present in this +environment, so the missing-tool guard is exercised for real, but the actual +ISO construction and whether OPNsense's Importer picks it up on a real boot +are UNTESTED. The exact genisoimage/xorriso invocation is a reasonable, +standard construction for a plain ISO9660 volume with one file at a fixed +path -- not independently verified. + +### 3. DOCFIX-146 (cont.) -- `opentofu/modules/opnsense-edge` +FILES (new): `opentofu/modules/opnsense-edge/{main,variables,outputs}.tf`. + +WHAT: one OPNsense edge VM per call. Mechanically identical to +`modules/cloudinit-vm`'s shape (a `backing_store` overlay disk + a secondary +volume attached as `cdrom`), but with the config-seed volume's +`create.content.url` pointing directly at the ISO9660 file built by script 2 +above -- no `libvirt_cloudinit_disk` resource involved (that's NoCloud-format, +wrong for this). Two explicit variables, `lan_network_name` and +`wan_network_name`, rather than a generic ordered list -- OPNsense's own +interface-1-is-LAN/interface-2-is-WAN convention makes list-ordering a real +silent-mistake risk, so the names are explicit instead. No PXE `boot`-order +attribute used (unlike `node-vm`): this VM boots from its own pre-installed +disk, not over the network, so the field flagged UNVERIFIED in +`node-vm/main.tf` doesn't need touching here at all. + +TWO THINGS FLAGGED, NOT PRESENTED AS FACT: (a) whether the Configuration +Importer's ISO9660 support behaves as described on a real boot -- the +research is well-sourced (a closed, milestone-targeted, core-developer- +assigned GitHub issue plus a corroborating later bug report) but not +independently tested this session; (b) whether `devices.interfaces` list +order reliably maps to which NIC FreeBSD/OPNsense enumerates first vs. +second -- assumed consistent with general libvirt XML ordering, not +independently confirmed for this specific guest OS. + +`config_iso_path` has no default and no fallback: the real `config.xml` +content for any site's LAN/WAN/routing role is design work that hasn't +happened. Not instantiated in root `main.tf`. + +`opentofu/README.md` updated: the "Scope of this delivery" and OPNsense +research sections both now describe this as BUILT (not just researched), +with the schema notes section gaining a new entry on `create.content.url` +accepting local filesystem paths (the mechanism both new scripts' outputs +rely on, inferred from `examples/alpine_cloudinit.tf`'s +`libvirt_cloudinit_disk.path` usage, not independently named in the docs +pages fetched this session). Workflow doc gap register item 4 and its +summary-table row updated to match; companion visual tracker (same Artifact +URL) redeployed. + +REVERT: `git rm -r opentofu/modules/opnsense-edge scripts/opnsense-prep-image.sh +scripts/opnsense-build-config-iso.sh tests/opnsense-prep-image +tests/opnsense-build-config-iso`; revert the `opentofu/README.md` and +workflow-doc sections if reverting fully. + +## Next actionable step + +Same underlying need as DOCFIX-144/145: a machine with the `tofu` binary, +`qemu-img`, and `genisoimage`/`xorriso` to actually run the full chain -- +`opnsense-prep-image.sh` for real, `opnsense-build-config-iso.sh` for real, +then `tofu apply` the module and boot it, to confirm the Configuration +Importer's ISO9660 behavior and the LAN/WAN interface-ordering assumption +against a real OPNsense instance rather than sourced-but-unverified research. diff --git a/docs/dc-dc-deployment-workflow.md b/docs/dc-dc-deployment-workflow.md index f67f1ef..fa16305 100644 --- a/docs/dc-dc-deployment-workflow.md +++ b/docs/dc-dc-deployment-workflow.md @@ -223,32 +223,31 @@ org ULA /48, and per-DC GUA carves to be NetBox-authoritative via "the extended import pipeline" -- that pipeline is not written. Blocks Stage 0's exit condition (literals must be real, not placeholders) and Stage 2. -4. **OPNsense edge deployment/config, and netem WAN-simulation -- (b)/(c) were - RESEARCHED 2026-07-09 (`opentofu/README.md`'s dedicated section, sourced - against OPNsense's own docs/forum/GitHub), (d) is still a real gap.** Each - site (Office1, DC1, DC2) gets its own independent OPNsense edge with a - simulated ISP uplink (D-100); OpenTofu owns creating the edge VMs (D-103). - Blocks Stage 1 (Office1 edge) and Stage 3 (per-DC edges) gates, which - require netem parameters "applied and measured." +4. **OPNsense edge deployment/config, and netem WAN-simulation -- (a)-(c) DONE + 2026-07-09, (d) is the only real gap left.** Each site (Office1, DC1, DC2) + gets its own independent OPNsense edge with a simulated ISP uplink (D-100); + OpenTofu owns creating the edge VMs (D-103). Blocks Stage 1 (Office1 edge) + and Stage 3 (per-DC edges) gates, which require netem parameters "applied + and measured." - **(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)+(c) The OPNsense VM/image AND its config -- RESEARCHED, confirmed - as ONE pattern, not built yet.** Cloud-init is confirmed unreliable on - OPNsense (FreeBSD) -- OPNsense's own forum consensus, not just a hedge. - The real native mechanism is the Configuration Importer (official docs): - scans an attached volume for `/conf/config.xml` in a 2-3 second - boot-time window; ISO9660 support was added to it specifically for - VM/cloud automation (attach a config ISO as a secondary CD-ROM) -- - mechanically IDENTICAL to `modules/cloudinit-vm`'s shape (a secondary - volume attached as `cdrom`), just a different payload (a plain ISO9660 - image with `/conf/config.xml`, not a NoCloud seed). Use the nano image - (pre-installed, serial-ready), not the installer images. Still needed to - actually build it: a preprocessing script for the nano image's - decompress+convert (OpenTofu's volume-download mechanism won't do that - itself), a way to build the config.xml ISO, and the actual config.xml - content (real design work). No image source formally chosen yet either. - Full sourcing in `opentofu/README.md`. + - **(b)+(c) The OPNsense VM/image AND its config -- BUILT + (`opentofu/modules/opnsense-edge`), UNVALIDATED.** Cloud-init is + confirmed unreliable on OPNsense (FreeBSD) -- OPNsense's own forum + consensus, not just a hedge. The real native mechanism is the + Configuration Importer (official docs): scans an attached volume for + `/conf/config.xml` in a 2-3 second boot-time window; ISO9660 support was + added to it specifically for VM/cloud automation (attach a config ISO as + a secondary CD-ROM) -- mechanically IDENTICAL to `modules/cloudinit-vm`'s + shape (a secondary volume attached as `cdrom`), just a different payload. + Built as `opentofu/modules/opnsense-edge` + two prep scripts + (`scripts/opnsense-prep-image.sh` for the nano image's decompress+ + convert, `scripts/opnsense-build-config-iso.sh` for the config.xml ISO). + UNVALIDATED: no `tofu` binary, no ISO-building tool, and no real + OPNsense boot were available to confirm any of it end-to-end. Real + `config.xml` content per site is still design work, no fallback + provided. Full sourcing in `opentofu/README.md`. - **(d) `tc netem` WAN-simulation -- a wholly separate mechanism, not an OPNsense setting.** No Terraform/OpenTofu-native equivalent exists at all (confirmed against the `libvirt_network` schema, gap #2); it is a @@ -302,7 +301,7 @@ | 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/node-VM PXE/cloud-init-VM patterns) | SCAFFOLDED 2026-07-09, UNVALIDATED (no `tofu` binary available this session) -- see gap #2 and `opentofu/README.md` | -| OPNsense image+config mechanism | RESEARCHED 2026-07-09 (config.xml ISO via the Configuration Importer, nano image) -- not yet built, see `opentofu/README.md` | +| OPNsense image+config mechanism (`modules/opnsense-edge`) | BUILT 2026-07-09, UNVALIDATED (no `tofu`/ISO-tool/real boot to confirm) -- real config.xml content per site still design work, see `opentofu/README.md` | | `opentofu/` (netem, DC2 planes) | NOT DONE -- 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 | diff --git a/docs/session-ledger.md b/docs/session-ledger.md index 57fb607..7f59fcf 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -806,8 +806,26 @@ sourced in `opentofu/README.md`. NOT built yet: a decompress/convert preprocessing script for the nano image, an ISO9660 config.xml-seed builder, and the real config.xml content -- itemized as the next - actionable step. Uncommitted -- operator has not yet asked to commit this - piece. + actionable step. COMMITTED + PUSHED (75ca823). +- **DONE -- DOCFIX-146 (changelog `docs/changelog-20260709-opnsense-edge-module.md`):** + operator asked to continue with OPNsense while the research was fresh. + Built all three itemized next-steps: `scripts/opnsense-prep-image.sh` + (nano image decompress+convert, no hardcoded mirror -- `OPNSENSE_MIRROR_BASE` + required), `scripts/opnsense-build-config-iso.sh` (ISO9660 config.xml + seed), and `opentofu/modules/opnsense-edge` wiring both together -- + mechanically identical to `cloudinit-vm`'s cdrom-attach shape, no + `libvirt_cloudinit_disk` involved. Explicit `lan_network_name`/ + `wan_network_name` variables (not an ordered list) to prevent a silent + LAN/WAN swap. Both harnesses green (4/4, 3/3) exercising REAL missing-tool + guards (`qemu-img`/`genisoimage`/`xorriso` are all genuinely absent this + session). Two things flagged, not presented as fact: whether the + Configuration Importer's ISO9660 behavior holds on a real boot, and + whether `devices.interfaces` list order reliably maps to LAN/WAN NIC + enumeration for this guest. `config_iso_path` has no default -- real + config.xml content per site is still undesigned. Not instantiated in root + `main.tf`. `opentofu/README.md` + workflow doc + visual tracker updated to + reflect BUILT (not just researched). Uncommitted -- operator has not yet + asked to commit this piece. diff --git a/opentofu/README.md b/opentofu/README.md index 6fbb450..22fdbdb 100644 --- a/opentofu/README.md +++ b/opentofu/README.md @@ -54,6 +54,21 @@ Linux cloud images) -- check that specifically before assuming this module covers OPNsense too. Neither module instantiated in root `main.tf` yet: no image source has been chosen for any Office1 service VM. +- `modules/opnsense-edge` (2026-07-09) -- one OPNsense edge VM per call, using + OPNsense's own Configuration Importer (NOT cloud-init -- see the dedicated + research section below) via a plain ISO9660 volume containing + `/conf/config.xml`, attached as a secondary cdrom disk -- mechanically + identical to `cloudinit-vm`'s seed-volume shape, no `libvirt_cloudinit_disk` + resource involved (wrong format for this). Paired with two new scripts that + do the OUT-OF-OPENTOFU preparation work: `scripts/opnsense-prep-image.sh` + (download the nano image, decompress, convert to qcow2, resize) and + `scripts/opnsense-build-config-iso.sh` (build the ISO9660 config-seed from + a real `config.xml`). Both scripts require external tools not present in + the session that wrote them (`qemu-img`; `genisoimage`/`xorriso`) -- + their harnesses only test the guard-clause paths, not the real + download/convert/build behavior; see each script's header. `config_iso_path` + has no default: real `config.xml` content per site is design work not done. + Not instantiated in root `main.tf`. - Root wiring for DC1 (planes + pool, using DC1's inherited DC0 CIDRs) and Office1 (pool only), plus all three mesh links. @@ -81,29 +96,30 @@ 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 edges -- four separable pieces, not one; (b) and (c) are now - RESEARCHED (2026-07-09, see the dedicated section below), not blocked:** - (a) the network substrate is done (`modules/mesh-link`, above); - (b)+(c) the image AND its config are a single confirmed pattern now (nano - image + a config.xml-seed ISO attached as cdrom, mechanically identical to - `modules/cloudinit-vm`'s shape) -- not yet built as a module, but no longer - an open research question, just implementation work; (d) `tc netem` WAN - simulation is a separate OS-level `tc qdisc` setting with no Terraform-native - equivalent at all, needing its own provisioner/script, and whose parameters - are still an unruled D-100 open item -- a decision, not just scripting, is - needed first. Full breakdown in `docs/dc-dc-deployment-workflow.md` gap - register item 4. +- **OPNsense edges -- (a)-(c) DONE 2026-07-09, only (d) remains:** (a) network + substrate (`modules/mesh-link`); (b)+(c) the image AND its config, built as + `modules/opnsense-edge` + the two prep scripts above -- UNVALIDATED (no real + boot, no `tofu` binary, no ISO-building tool available this session; see + each file's header for exactly what's confirmed vs. assumed); real + `config.xml` content per site is still design work, not yet done, and the + module has no fallback for it. (d) `tc netem` WAN simulation is a separate + OS-level `tc qdisc` setting with no Terraform-native equivalent at all, + needing its own provisioner/script, and whose parameters are still an + unruled D-100 open item -- a decision, not just scripting, is needed first. + Full breakdown in `docs/dc-dc-deployment-workflow.md` gap register item 4. - **MAAS-region / NetBox / GitBucket service VMs -- module exists (`cloudinit-vm`), not instantiated.** No image source has been chosen for any of them, and their actual `user_data`/`meta_data`/`network_config` content (what packages, what config, what static address) is real design work that hasn't been done -- required inputs, no invented placeholders. -## OPNsense deployment research (2026-07-09) -- read before building the OPNsense module +## OPNsense deployment research (2026-07-09) -- the basis for `modules/opnsense-edge` Researched directly against OPNsense's own docs/forum/GitHub, not inferred -- -confirms `modules/cloudinit-vm` genuinely does NOT apply, AND identifies the -real mechanism, which turns out to need the same mechanical shape. +confirms `modules/cloudinit-vm` genuinely does NOT apply, identifies the real +mechanism (which turns out to need the same mechanical shape), and is now +implemented as `modules/opnsense-edge` above. Read this before changing that +module -- it's the "why," not just background. 1. **Cloud-init is confirmed unreliable on OPNsense, not just unconfirmed.** Community consensus on OPNsense's own forum: "FreeBSD cloud-init support, @@ -154,16 +170,18 @@ way `modules/node-vm`'s first-interface-gets-PXE-boot-priority does. (https://docs.opnsense.org/manual/install.html) -**What building the module still needs, not yet done:** (a) a small -preprocessing script for step 5's decompress+convert -- `create.content.url` -almost certainly does a plain HTTP fetch with no decompression, so it can't -consume the `.img.bz2` directly; the qcow2 needs to exist BEFORE OpenTofu -sees it. (b) a way to build a plain ISO9660 image containing `/conf/config.xml` -(e.g. `genisoimage`/`xorriso` -- exact tool/flags not yet verified against a -real build) to feed into a `libvirt_volume`'s `create.content.url`, the same -way `libvirt_cloudinit_disk.path` feeds `cloudinit-vm`'s seed volume. (c) an -actual `config.xml` for OPNsense's own LAN/WAN/routing role -- real design -work, not yet done. +**Built from this research (2026-07-09):** `scripts/opnsense-prep-image.sh` +(step 5's decompress+convert, since `create.content.url` almost certainly +does a plain HTTP fetch with no decompression and can't consume the +`.img.bz2` directly), `scripts/opnsense-build-config-iso.sh` (a plain ISO9660 +image containing `/conf/config.xml`, to feed a `libvirt_volume`'s +`create.content.url` the same way `libvirt_cloudinit_disk.path` feeds +`cloudinit-vm`'s seed volume), and `modules/opnsense-edge` wiring both +together. **Still not done:** the actual `config.xml` content for OPNsense's +own LAN/WAN/routing role at each site -- real design work, no fallback +provided; and real-world verification of the whole chain (no `tofu` binary, +no ISO-building tool, and no real OPNsense boot were available this session +to confirm any of it end-to-end). ## Schema notes (read before extending) @@ -223,6 +241,16 @@ -- libvirt's actual pool-relative path convention was not independently confirmed this session, and a real `.path` attribute reference is always available and unambiguous instead. +- `create.content.url` accepts plain LOCAL filesystem paths, not just http(s) + URLs -- inferred from `examples/alpine_cloudinit.tf` assigning + `libvirt_cloudinit_disk.alpine_seed.path` (a local path, not a URL) + directly to `create.content.url`. `modules/opnsense-edge` relies on this + for BOTH its base image (`scripts/opnsense-prep-image.sh`'s output path) + and its config-seed ISO (`scripts/opnsense-build-config-iso.sh`'s output + path) -- neither is fetched from a real URL. Reasonably high confidence + (it's exactly what the real example does), but the mechanism wasn't named + explicitly in the docs pages fetched this session -- confirm with + `tofu plan` if it errors on a `file://`-less bare path. ## Conventions carried from the rest of this repo diff --git a/opentofu/modules/opnsense-edge/main.tf b/opentofu/modules/opnsense-edge/main.tf new file mode 100644 index 0000000..e8019af --- /dev/null +++ b/opentofu/modules/opnsense-edge/main.tf @@ -0,0 +1,134 @@ +# opnsense-edge: one OPNsense edge VM (D-100's per-site independent OPNsense +# edge with a simulated ISP uplink; D-103: OpenTofu owns creating it). +# +# NOT the cloud-init pattern (modules/cloudinit-vm) -- confirmed unreliable +# on OPNsense/FreeBSD this session (see opentofu/README.md's "OPNsense +# deployment research" section, fully sourced). This module instead uses +# OPNsense's own Configuration Importer: a plain ISO9660 volume containing +# /conf/config.xml, attached as a secondary cdrom disk -- mechanically +# IDENTICAL to cloudinit-vm's seed-volume shape (a libvirt_volume with +# create.content.url, attached as device="cdrom"), just a different payload +# and no libvirt_cloudinit_disk resource (that resource is NoCloud-specific; +# wrong format here). Both the base image and the config ISO are prepared +# OUTSIDE OpenTofu by scripts/opnsense-prep-image.sh and +# scripts/opnsense-build-config-iso.sh respectively -- see each variable's +# description for why. +# +# UNVERIFIED, flagged plainly (see opentofu/README.md for the full account): +# (a) whether the Configuration Importer's ISO9660 support actually behaves +# as described once booted for real -- the research is well-sourced but not +# independently tested this session (no real OPNsense boot, no +# genisoimage/xorriso available); (b) whether `devices.interfaces` list +# ORDER reliably maps to which NIC FreeBSD/OPNsense enumerates as its first +# vs. second interface (assumed here, consistent with libvirt XML ordering +# generally, but not independently confirmed for this specific guest OS). +# Confirm both on a real boot before trusting LAN/WAN role assignment. +# +# No PXE boot-order attribute is used here (unlike modules/node-vm): this VM +# boots from its own pre-installed disk, not over the network, so the +# UNVERIFIED `boot`-order shape noted in node-vm/main.tf does not need +# touching for this module at all. + +resource "libvirt_volume" "disk" { + name = "${var.vm_name}-disk.qcow2" + pool = var.pool_name + capacity = var.disk_size_bytes + + target = { + format = { + type = "qcow2" + } + } + + backing_store = { + path = var.base_volume_path + format = { + type = "qcow2" + } + } +} + +resource "libvirt_volume" "config_seed" { + name = "${var.vm_name}-config.iso" + pool = var.pool_name + + create = { + content = { + url = var.config_iso_path + } + } +} + +resource "libvirt_domain" "vm" { + 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" + } + }, + { + device = "cdrom" + source = { + volume = { + pool = libvirt_volume.config_seed.pool + volume = libvirt_volume.config_seed.name + } + } + target = { + dev = "sda" + bus = "sata" + } + } + ] + + # order matters: index 0 is LAN, index 1 is WAN, matching OPNsense's own + # interface-1-is-LAN/interface-2-is-WAN default convention. See the + # UNVERIFIED note above -- this ordering assumption is not independently + # confirmed for this specific guest. + interfaces = [ + { + model = { + type = "virtio" + } + source = { + network = { + network = var.lan_network_name + } + } + }, + { + model = { + type = "virtio" + } + source = { + network = { + network = var.wan_network_name + } + } + } + ] + } +} diff --git a/opentofu/modules/opnsense-edge/outputs.tf b/opentofu/modules/opnsense-edge/outputs.tf new file mode 100644 index 0000000..45d3867 --- /dev/null +++ b/opentofu/modules/opnsense-edge/outputs.tf @@ -0,0 +1,9 @@ +output "domain_id" { + description = "libvirt domain id." + value = libvirt_domain.vm.id +} + +output "domain_name" { + description = "libvirt domain name." + value = libvirt_domain.vm.name +} diff --git a/opentofu/modules/opnsense-edge/variables.tf b/opentofu/modules/opnsense-edge/variables.tf new file mode 100644 index 0000000..47b1e5b --- /dev/null +++ b/opentofu/modules/opnsense-edge/variables.tf @@ -0,0 +1,67 @@ +variable "vm_name" { + description = "Domain name, e.g. \"office1-opnsense\", \"dc1-opnsense\"." + type = string +} + +variable "memory_mib" { + description = "Memory in MiB. No default -- pass explicitly (OPNsense sizing hasn't been decided; check OPNsense's own hardware-sizing guidance for the expected role before picking a value)." + type = number +} + +variable "vcpu" { + description = "Virtual CPU count. No default -- pass explicitly." + type = number +} + +variable "pool_name" { + description = "libvirt_pool this VM's disk + config-seed volume live in." + type = string +} + +variable "disk_size_bytes" { + description = "Boot disk size in bytes (the backing_store overlay's capacity, not the base nano image's own size). No default -- pass explicitly." + type = number +} + +variable "base_volume_path" { + description = <<-EOT + Host filesystem path of the PREPARED (decompressed, qcow2-converted, + resized) OPNsense nano image -- the output of + `scripts/opnsense-prep-image.sh`, typically fed through a + `modules/base-image` call (pass that module's `path` output here; the + same real-attribute-reference pattern modules/cloudinit-vm uses). Do NOT + point this at the raw downloaded .img.bz2 -- libvirt_volume's + create.content.url almost certainly does a plain fetch with no + decompression/conversion; that work must already be done. + EOT + type = string +} + +variable "config_iso_path" { + description = <<-EOT + Host filesystem path of a plain ISO9660 image containing + /conf/config.xml -- the output of + `scripts/opnsense-build-config-iso.sh`. This is OPNsense's own + Configuration Importer mechanism (NOT cloud-init -- confirmed unreliable + on OPNsense/FreeBSD; see opentofu/README.md's OPNsense research section). + No default: the real config.xml content for this site's LAN/WAN/routing + role is design work that has to happen first -- do not point this at a + placeholder/empty config.xml to make the module "work" sooner. + EOT + type = string +} + +variable "lan_network_name" { + description = <<-EOT + libvirt_network name this edge's LAN-side interface attaches to. OPNsense + defaults interface 1 = LAN (see opentofu/README.md) -- kept as an + explicit named variable rather than an ordered list, deliberately, so a + LAN/WAN swap can't happen silently via list-ordering. + EOT + type = string +} + +variable "wan_network_name" { + description = "libvirt_network name this edge's WAN-side interface attaches to (OPNsense's interface 2 = WAN convention -- see opentofu/README.md). The mesh-link legs (modules/mesh-link) or a per-site ISP-simulated segment are the expected callers here." + type = string +} diff --git a/scripts/opnsense-build-config-iso.sh b/scripts/opnsense-build-config-iso.sh new file mode 100644 index 0000000..802484f --- /dev/null +++ b/scripts/opnsense-build-config-iso.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# scripts/opnsense-build-config-iso.sh +# +# Builds a plain ISO9660 image containing /conf/config.xml, for OPNsense's +# Configuration Importer (see opentofu/README.md's "OPNsense deployment +# research" section) -- ISO9660 support was added to the Importer +# specifically so a config ISO could be attached as a secondary CD-ROM, +# mechanically identical to how modules/cloudinit-vm attaches its NoCloud +# seed via a libvirt_volume + create.content.url pointing at a local path. +# +# NOTE: the genisoimage/xorriso invocation below is a reasonable, standard +# construction for a plain ISO9660 volume containing one file at a fixed +# path -- it has NOT been independently verified against a real OPNsense +# boot this session (neither tool nor a real OPNsense VM was available to +# test with). Confirm the Configuration Importer actually picks this up +# before relying on it operationally. +# +# Requires: genisoimage or xorriso. Read-only w.r.t. the repo; writes only +# to and a self-cleaning temp dir. +# Exit: 0 built | 1 bad input | 2 required tool missing. +set -euo pipefail + +CONFIG_XML="${1:?usage: opnsense-build-config-iso.sh }" +OUT="${2:?usage: opnsense-build-config-iso.sh }" + +[ -r "$CONFIG_XML" ] || { echo "FAIL: cannot read $CONFIG_XML"; exit 1; } + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/conf" +cp "$CONFIG_XML" "$TMP/conf/config.xml" + +if command -v genisoimage >/dev/null 2>&1; then + genisoimage -o "$OUT" -V "OPNSENSE_CFG" -J -R "$TMP" +elif command -v xorriso >/dev/null 2>&1; then + xorriso -as genisoimage -o "$OUT" -V "OPNSENSE_CFG" -J -R "$TMP" +else + echo "FAIL: genisoimage or xorriso required on PATH" + exit 2 +fi + +echo "PASS: built $OUT (contains /conf/config.xml)" diff --git a/scripts/opnsense-prep-image.sh b/scripts/opnsense-prep-image.sh new file mode 100644 index 0000000..ee86cc4 --- /dev/null +++ b/scripts/opnsense-prep-image.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# scripts/opnsense-prep-image.sh [] +# +# Downloads the OPNsense nano image (pre-installed, serial-console-ready -- +# see opentofu/README.md's "OPNsense deployment research" section for why +# nano, not the vga/serial/dvd installer images), decompresses it, converts +# raw -> qcow2, and grows it. Output: a ready qcow2 file at +# -- feed that LOCAL PATH as modules/base-image's +# source_url (create.content.url accepts local paths, confirmed via +# examples/alpine_cloudinit.tf's libvirt_cloudinit_disk.path usage pattern). +# +# `libvirt_volume`'s create.content.url almost certainly does a plain fetch +# with no decompression/format-conversion -- this script does that work +# OUTSIDE OpenTofu, once, before `tofu apply` ever sees the result. +# +# Requires: curl or wget, bunzip2, qemu-img. Read-only w.r.t. the repo; +# writes only to and a self-cleaning temp dir. +# Exit: 0 prepared | 1 bad input | 2 required tool missing. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +VERSION="${1:?usage: opnsense-prep-image.sh []}" +OUT="${2:?usage: opnsense-prep-image.sh []}" +GROW="${3:-+8G}" + +# MIRROR: no single canonical URL is hardcoded here -- OPNsense publishes +# multiple mirrors (see https://opnsense.org/download/) and this repo's own +# "never use an inferred value" discipline (hard rule 2) applies just as +# much to a download source as to a CIDR or a hostname. Defaulting silently +# to one specific mirror risks it going stale/unavailable without anyone +# noticing until a real deploy fails partway through. +MIRROR_BASE="${OPNSENSE_MIRROR_BASE:?set OPNSENSE_MIRROR_BASE to a real OPNsense mirror base URL (see https://opnsense.org/download/) -- not hardcoded here, mirrors change}" + +for bin in bunzip2 qemu-img; do + command -v "$bin" >/dev/null 2>&1 || { echo "FAIL: $bin required on PATH"; exit 2; } +done +if command -v curl >/dev/null 2>&1; then + FETCH=(curl -fSL -o) +elif command -v wget >/dev/null 2>&1; then + FETCH=(wget -O) +else + echo "FAIL: curl or wget required on PATH" + exit 2 +fi + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +IMG_BZ2="$TMP/OPNsense-${VERSION}-nano-amd64.img.bz2" +URL="${MIRROR_BASE%/}/releases/${VERSION}/OPNsense-${VERSION}-nano-amd64.img.bz2" + +echo "== downloading: $URL ==" +"${FETCH[@]}" "$IMG_BZ2" "$URL" + +echo "== decompressing ==" +bunzip2 -k "$IMG_BZ2" +IMG_RAW="${IMG_BZ2%.bz2}" + +echo "== converting raw -> qcow2 ==" +qemu-img convert -f raw -O qcow2 "$IMG_RAW" "$OUT" + +echo "== growing by $GROW ==" +qemu-img resize "$OUT" "$GROW" + +echo "PASS: prepared $OUT" diff --git a/tests/opnsense-build-config-iso/run-tests.sh b/tests/opnsense-build-config-iso/run-tests.sh new file mode 100644 index 0000000..1ad46b3 --- /dev/null +++ b/tests/opnsense-build-config-iso/run-tests.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# tests/opnsense-build-config-iso/run-tests.sh -- offline harness for +# scripts/opnsense-build-config-iso.sh. Only tests the guard clauses this +# environment can actually exercise. Whether the built ISO is actually +# picked up by OPNsense's Configuration Importer is UNTESTED here (no +# genisoimage/xorriso and no real OPNsense VM were available this session) +# -- logged as residual risk, not hidden; see opentofu/README.md. +# Exit: 0 all pass | 1 any case failed. ASCII + LF. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$HERE/../../scripts/opnsense-build-config-iso.sh" +PASS=0; FAIL=0 + +run() { # run