diff --git a/docs/changelog-20260709-opnsense-config-design.md b/docs/changelog-20260709-opnsense-config-design.md new file mode 100644 index 0000000..ca15128 --- /dev/null +++ b/docs/changelog-20260709-opnsense-config-design.md @@ -0,0 +1,80 @@ +# Changelog 2026-07-09 -- OPNsense config.xml design (DOCFIX-149) + +No live infrastructure touched. Real design work, not a placeholder -- +researched OPNsense's own real, currently-shipped config.xml before +drafting, per this session's established discipline. + +## Item + +### 1. DOCFIX-149 -- `opentofu/templates/opnsense-config.xml.tmpl` + renderer +FILES (new): `opentofu/templates/opnsense-config.xml.tmpl`, +`opentofu/templates/README.md`, `scripts/opnsense-render-config.sh`, +`tests/opnsense-render-config/run-tests.sh`. + +WHAT: a `{{TOKEN}}`-parameterized OPNsense `config.xml` template (this +repo's existing clientdocs token convention, reused rather than inventing a +new one), covering system/interfaces/gateway/staticroutes/DNS/NTP and +D-107-shaped firewall rules (default-deny WAN egress except NTP + the +per-DC mirror's upstream sync -- the two exceptions D-107 names). Paired +with `scripts/opnsense-render-config.sh`, which substitutes tokens from +required environment variables (no invented defaults except the NTP pool, +which defaults to the exact same public pool OPNsense's own sample ships -- +a real, confirmed default, not an invention) and feeds the result to the +already-existing `scripts/opnsense-build-config-iso.sh` -> ISO9660 -> +`modules/opnsense-edge` pipeline. + +VERIFICATION: built directly from OPNsense's own real `config.xml.sample` +(`opnsense/core`'s repo, `src/etc/config.xml.sample`) fetched and read as +real content, not summarized -- the same discipline that caught the +DOCFIX-142 syntax bug and the DOCFIX-148 LAN/WAN documentation error. This +fetch also DIRECTLY CONFIRMED the DOCFIX-148 audit finding: the real sample +ships literal placeholder device names (`mismatch0`/`mismatch1`) inside each +interface's own `` element, proving LAN/WAN role assignment is exactly +the per-block explicit mapping DOCFIX-148 concluded it was, now with a real +example rather than research-by-inference. The `staticroutes` schema was +confirmed from `opnsense/core`'s own `Route.xml` MVC model file (the actual +schema-defining source, not a doc page) -- `network`/`gateway`/`descr`/ +`enabled` fields, and `gateway` must reference a named gateway object +(hence the template's own `` addition, +which the stock sample doesn't need since it uses DHCP). + +TESTED END-TO-END, not just guard clauses: unlike every other opnsense-* +script this session, `opnsense-render-config.sh` needs no external tool +(pure bash + template substitution), so its harness (8/8) exercises the +real behavior -- happy path, well-formed-XML output (verified with +`python3 -m xml.dom.minidom`), the NTP real-default fallback, one static +route rendering while an unset second slot correctly emits nothing, and +three failure paths. + +**A real bug the harness itself caught:** the token `HOSTNAME` collides +with bash's own built-in `$HOSTNAME` variable -- `unset HOSTNAME` does not +actually clear it (bash repopulates it), so a "missing required token" +test that should have failed instead silently passed with the shell's own +system hostname. Renamed to `OPNSENSE_HOSTNAME` throughout (template, +script, docs, tests) before this shipped, not after. + +WHAT'S STILL NOT REAL: `opentofu/templates/README.md`'s token legend marks +each token's status precisely -- several cannot be filled with a genuine +value yet because the decision they depend on is still PROPOSED +(D-100/D-101/D-107's WAN addressing, LAN plane assignment, mirror +software choice), and two (`WAN_IF`/`LAN_IF`) are only measurable on a real +boot (which `vtnetN` a network becomes). `ROOT_PASSWORD_HASH` must be +freshly generated per deployment -- the stock sample's own shipped hash is +a well-known default and must never be reused verbatim. + +`opentofu/README.md`'s OPNsense research section and gap register item 4 +in `docs/dc-dc-deployment-workflow.md` (+ its summary table, + the +companion visual tracker) updated to reflect this as BUILT (the mechanism +and the tested renderer), not just the still-pending final values. + +REVERT: `git rm -r opentofu/templates scripts/opnsense-render-config.sh +tests/opnsense-render-config`; revert the `opentofu/README.md` and +workflow-doc sections if reverting fully. + +## Next actionable step + +Two independent things still block a real per-site config.xml: (1) Stage 0 +ratification of D-100/D-101/D-107 (WAN addressing, LAN plane assignment, +mirror software choice) -- a decision, not a build task; (2) a real boot to +measure `vtnetN` device assignment before setting `WAN_IF`/`LAN_IF`. Neither +needs more OpenTofu/script work -- the mechanism is done and tested. diff --git a/docs/dc-dc-deployment-workflow.md b/docs/dc-dc-deployment-workflow.md index 6b6001c..7705570 100644 --- a/docs/dc-dc-deployment-workflow.md +++ b/docs/dc-dc-deployment-workflow.md @@ -255,10 +255,19 @@ 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`. + The real `config.xml` DESIGN is now also done: + `opentofu/templates/opnsense-config.xml.tmpl` (a `{{TOKEN}}`-parameterized + template built from OPNsense's own real shipped sample, covering + interfaces/gateway/staticroutes/NAT/DNS/NTP and D-107-shaped firewall + rules) + `scripts/opnsense-render-config.sh` (the renderer -- fully + tested end-to-end, 8/8, unlike the other opnsense-* scripts, since it + needs no external tool). UNVALIDATED regardless: no `tofu` binary, no + ISO-building tool, and no real OPNsense boot were available to confirm + the whole chain end-to-end. Several template tokens still can't be + filled with real per-site values -- pending Stage 0 ratification + (D-100/D-101/D-107) or only measurable on a real boot (which `vtnetN` + device a network becomes). Full sourcing in `opentofu/README.md` and + `opentofu/templates/README.md`'s token legend. - **(d) `tc netem` WAN-simulation -- mechanism BUILT (`modules/netem-link`), only the real parameters remain.** No Terraform/OpenTofu-native equivalent exists (confirmed against the `libvirt_network` schema, gap @@ -313,7 +322,8 @@ | 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 (`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` | +| OPNsense image+config mechanism (`modules/opnsense-edge`) | BUILT 2026-07-09, UNVALIDATED (no `tofu`/ISO-tool/real boot to confirm), see `opentofu/README.md` | +| OPNsense config.xml design (`templates/opnsense-config.xml.tmpl`) | BUILT 2026-07-09, renderer fully tested (8/8) -- several tokens still pending Stage 0 ratification or a real boot, see `opentofu/templates/README.md` | | MAAS VM-host registration (`modules/maas-vm-host`) | BUILT 2026-07-09, UNVALIDATED -- needs a real MAAS zone/pool + vcloud power_address, see `opentofu/README.md` | | `tc netem` mechanism (`modules/netem-link`) | BUILT 2026-07-09, UNVALIDATED -- real latency/jitter/loss/rate parameters still an unruled D-100 item, see `opentofu/README.md` | | `opentofu/` (DC2 planes) | NOT DONE -- deliberately deferred pending NetBox CIDR assignment, see `opentofu/README.md` | diff --git a/docs/session-ledger.md b/docs/session-ledger.md index 7627066..9cfd246 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -878,7 +878,30 @@ doc-summarized prose for syntax questions). New SKILL.md routing entry; cross-referenced from `opentofu/README.md` in both directions, no content duplicated (README.md stays the single source of truth for per-module - findings). Uncommitted -- operator has not yet asked to commit this piece. + findings). COMMITTED + PUSHED (96ecbad). +- **DONE -- DOCFIX-149 (changelog `docs/changelog-20260709-opnsense-config-design.md`):** + operator asked to move into designing the real config.xml content. + Fetched OPNsense's own real, currently-shipped `config.xml.sample` + (`opnsense/core` repo) and its `Route.xml` static-routes model before + drafting. Built `opentofu/templates/opnsense-config.xml.tmpl` + (`{{TOKEN}}`-parameterized, reusing this repo's existing clientdocs + convention) + `scripts/opnsense-render-config.sh`. The real sample fetch + DIRECTLY CONFIRMED the DOCFIX-148 audit finding with an actual example: + ships literal placeholder device names inside each interface's own `` + element, proving LAN/WAN role assignment really is the explicit per-block + mapping DOCFIX-148 concluded. **The renderer is tested END-TO-END, not + just guard clauses** (8/8 -- it needs no external tool, a first among the + opnsense-* scripts) -- and the harness caught a REAL bug before it shipped: + the token `HOSTNAME` collides with bash's own built-in `$HOSTNAME` + variable (`unset` doesn't actually clear it), silently passing a test that + should have failed. Renamed to `OPNSENSE_HOSTNAME` throughout. Full token + legend in `opentofu/templates/README.md`, marking exactly which values + are real (NTP pool, D-106 naming) vs. still pending Stage 0 ratification + (D-100/D-101/D-107) vs. only measurable on a real boot (`vtnetN` + assignment) vs. a security requirement (root password hash must be + freshly generated, never the stock sample's own shipped default). + `opentofu/README.md` + workflow doc + visual tracker updated. Uncommitted + -- operator has not yet asked to commit this piece. diff --git a/opentofu/README.md b/opentofu/README.md index 06da703..007fef3 100644 --- a/opentofu/README.md +++ b/opentofu/README.md @@ -70,8 +70,24 @@ 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`. + has no default. Not instantiated in root `main.tf`. +- `templates/opnsense-config.xml.tmpl` + `scripts/opnsense-render-config.sh` + (2026-07-09) -- the actual `config.xml` DESIGN, not just the mechanism: + a `{{TOKEN}}`-parameterized template built from OPNsense's own real, + currently-shipped `config.xml.sample` (confirms the LAN/WAN role is set + via each interface block's own `` device name, not declaration order + -- see the "Audit pass" section below) plus the confirmed `staticroutes` + schema, covering system/interfaces/gateway/staticroutes/NAT/DNS/NTP and + D-107-shaped firewall rules (default-deny WAN egress except NTP + mirror + sync). The renderer is FULLY TESTED end-to-end (needs no external tool, + unlike the other opnsense-* scripts) -- 8/8 harness green, including a + real bug the harness itself caught: the token `HOSTNAME` collides with + bash's own built-in `$HOSTNAME` variable (renamed to + `OPNSENSE_HOSTNAME`). Full token legend, what's a real value vs. a + still-pending decision (D-100/D-101/D-107, or something only measurable + on a real boot), in `templates/README.md`. No per-site config.xml written + yet -- several tokens genuinely cannot be filled until Stage 0 ratifies + the decisions they depend on. - `modules/maas-vm-host` (2026-07-09) -- registers the vcloud host's virsh/ libvirt connection with MAAS as a VM host, via the official `canonical/maas` provider's `maas_vm_host` resource (D-103: "OpenTofu registers each DC's @@ -194,11 +210,15 @@ 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). +together. **Done as of a follow-on 2026-07-09 delivery:** the actual +`config.xml` DESIGN -- see `templates/opnsense-config.xml.tmpl` + +`templates/README.md` for the full, tested template and token legend. +**Still not done:** the final per-site values for several tokens (pending +Stage 0 ratification of D-100/D-101/D-107, or only measurable on a real +boot); and real-world verification of the whole chain end-to-end (no `tofu` +binary, no ISO-building tool, and no real OPNsense boot were available this +session to confirm any of it, though the template renderer itself is fully +tested and needs none of those). ## MAAS registration + tc netem research (2026-07-09) -- the basis for `modules/maas-vm-host` and `modules/netem-link` diff --git a/opentofu/templates/README.md b/opentofu/templates/README.md new file mode 100644 index 0000000..2ae5226 --- /dev/null +++ b/opentofu/templates/README.md @@ -0,0 +1,66 @@ +# opentofu/templates/ -- OPNsense config.xml template + +`opnsense-config.xml.tmpl` is the `{{TOKEN}}`-parameterized OPNsense +`config.xml` for a DC-DC edge (Office1, DC1, or DC2 -- one instantiation per +site). Built directly from OPNsense's own real, currently-shipped +`config.xml.sample` (`opnsense/core`, fetched 2026-07-09 -- see +`.claude/skills/openstack-cloud-ops/references/opentofu-provider-docs.md` +for the fetch method) plus the confirmed `staticroutes` schema +(`opnsense/core`'s `Route.xml` model). This is real design work, not a +placeholder -- but several tokens genuinely cannot be filled with a real +value yet because the decisions they depend on (D-100/D-101/D-107) are +still PROPOSED, or because the value can only be measured on a real boot. +Never invent a value for those; leave the token required and unfilled until +the real one exists. + +## Pipeline + +``` +opnsense-config.xml.tmpl + --(scripts/opnsense-render-config.sh, env-var tokens)--> +real config.xml + --(scripts/opnsense-build-config-iso.sh)--> +config.iso (plain ISO9660, /conf/config.xml) + --(opentofu/modules/opnsense-edge, config_iso_path variable)--> +attached as a secondary cdrom disk to the domain +``` + +## Token legend + +| Token | Status | Notes | +|---|---|---| +| `{{OPNSENSE_HOSTNAME}}` | Design choice, not yet made | e.g. `opnsense`. NOT named `HOSTNAME` -- that collides with bash's own built-in `$HOSTNAME` variable (caught by this delivery's own test harness: `unset HOSTNAME` doesn't actually clear it, since bash repopulates it). | +| `{{DOMAIN}}` | Real, applies D-106's naming convention | e.g. `omega.dc1.vr1.cloud.neumatrix.local`, following the (still-PROPOSED, but already-settled-as-design-intent) `.omega..vr1.cloud.neumatrix.local` pattern -- same status as DC1's plane CIDRs elsewhere in this repo: carried-forward design intent, not invented. | +| `{{ROOT_PASSWORD_HASH}}` | MUST be freshly generated, never reused | The real `config.xml.sample` ships a bcrypt hash (`$2y$10$...`) for a well-known default OPNsense password -- reusing it verbatim would mean every edge shares a public default root password, a real exposure. Generate a fresh one per deployment, e.g. `htpasswd -bnBC 10 "" '' \| tr -d ':\n'` (produces a `$2y$10$...`-compatible bcrypt hash) -- NOT independently verified against a real OPNsense login this session; confirm login works before relying on it operationally. | +| `{{NTP_UPSTREAM_SERVERS}}` / `{{NTP_PREFER_SERVER}}` | Real default supplied (optional to override) | Defaults to the exact same public pool OPNsense's own sample ships (`N.opnsense.pool.ntp.org`) -- a real, confirmed default, not invented. D-107 says the edge "syncs upstream to internet NTP pools," consistent with keeping this default. Override only if a different upstream is decided. | +| `{{WAN_IF}}` / `{{LAN_IF}}` | MUST be measured on a real boot | The `` value is the actual `vtnetN` (or similar) device name FreeBRSD assigns -- confirmed from the real sample that this, not interface declaration order, is what assigns the LAN/WAN role (see `opentofu/README.md`'s "Audit pass" section for the full account of this correction). Boot the domain, run `ifconfig`, and set these to match `opentofu/modules/opnsense-edge`'s actual `lan_network_name`/`wan_network_name` wiring. | +| `{{WAN_IPADDR}}` / `{{WAN_SUBNET_BITS}}` / `{{WAN_GATEWAY}}` | Pending D-100/D-101 | The "simulated ISP uplink" network's real addressing isn't assigned yet (NetBox-pending). | +| `{{LAN_IPADDR}}` / `{{LAN_SUBNET_BITS}}` | Pending a design decision | WHICH plane(s) OPNsense's LAN interface actually serves (metal-admin? provider-public? something else?) is not fully specified in the buildout design as read this session -- confirm before filling this in, don't assume. | +| `{{MIRROR_SYNC_PROTOCOL}}` / `{{MIRROR_UPSTREAM_NET}}` / `{{MIRROR_SYNC_PORT}}` | Pending -- mirror software not chosen | D-107 requires the per-DC artifact mirror's own upstream sync as one of exactly two allowed WAN egress paths (the other is NTP), but doesn't specify which mirror software (apt-mirror? aptly? a generic reverse proxy?) -- the port/protocol depend on that choice. Tooling gap register item 3 territory. | +| `{{ROUTE1_NETWORK}}`/`{{ROUTE1_GATEWAY}}`/`{{ROUTE1_DESCR}}`, `{{ROUTE2_*}}` | Optional, pending DC2 CIDR assignment | At most 2 static routes needed per the known topology (a peer-DC replication-plane route at DC1/DC2; an Office1-management route). Leave a slot's `*_NETWORK` var unset to omit it entirely -- `scripts/opnsense-render-config.sh` drops an unset slot rather than emitting an empty/invalid ``. DC2's own CIDRs aren't assigned yet (D-101 open item), so DC1's route to DC2 can't be filled in until then. | + +## Design choices baked into the template (not tokens -- read before assuming these are wrong) + +- **Firewall: default-deny WAN egress, explicit NTP + mirror-sync allow rules** + (D-107's "controlled egress ... narrowly scoped: NTP-only, plus the + mirror's upstream sync"). The two default LAN-allow rules are kept exactly + as OPNsense's own sample ships them (basic operation depends on them). +- **`gateways.gateway_item`** is new versus the stock sample (which only has + an implicit DHCP-derived gateway) -- required because the WAN side here is + a static "simulated ISP uplink," not DHCP, and `staticroutes.route.gateway` + must reference a named gateway object (confirmed from the real + `Route.xml` model: `gateway` is a `JsonKeyValueStoreField` needing "a valid + gateway from the list"). Named `WAN_GW` -- change consistently in both + places if you rename it. +- **`unbound`/`nat`/`rrd`/legacy ``** sections are left exactly as + the real stock sample ships them -- minimal, working defaults, not + independently redesigned. + +## What this delivery does NOT do + +Write the actual, final, per-site `config.xml` files (Office1/DC1/DC2) -- +that needs the pending values above filled in with real ones, which in turn +needs Stage 0 ratification (D-100/D-101/D-107) and a real boot to measure +`vtnetN` assignment. This delivery is the mechanism (template + a tested +renderer), consistent with everything else built this session: parameterized +and reusable, not three files with invented IP addresses. diff --git a/opentofu/templates/opnsense-config.xml.tmpl b/opentofu/templates/opnsense-config.xml.tmpl new file mode 100644 index 0000000..1b1e1f0 --- /dev/null +++ b/opentofu/templates/opnsense-config.xml.tmpl @@ -0,0 +1,176 @@ + + + + opnsense + + normal + {{OPNSENSE_HOSTNAME}} + {{DOMAIN}} + 1 + + + admins + System Administrators + system + 1999 + 0 + page-all + + + root + System Administrator + system + admins + {{ROOT_PASSWORD_HASH}} + 0 + + Etc/UTC + {{NTP_UPSTREAM_SERVERS}} + + https + + yes + 1 + + 1 + + monthly + + 1 + + admins + + + + + 1 + {{WAN_IF}} + + {{WAN_IPADDR}} + {{WAN_SUBNET_BITS}} + WAN_GW + 1 + 1 + + + 1 + {{LAN_IF}} + {{LAN_IPADDR}} + {{LAN_SUBNET_BITS}} + + + + + wan + {{WAN_GATEWAY}} + WAN_GW + 1 + inet + simulated ISP uplink gateway (D-100) + + + +{{ROUTE1_BLOCK}} +{{ROUTE2_BLOCK}} + + + 1 + + + + automatic + + + + + + + + + {{NTP_PREFER_SERVER}} + {{NTP_UPSTREAM_SERVERS}} + + + + + + + 1 + keep + 1 + pass + 1 + lan + in + inet + any + lan + any + Default allow LAN to any rule + + + 1 + keep + 11 + pass + 1 + lan + in + inet6 + any + lan + any + Default allow LAN IPv6 to any rule + + + 1 + keep + 20 + pass + 1 + wan + out + inet + UDP + {{WAN_IPADDR}} + any + 123 + D-107 controlled egress: NTP only, WAN uplink + + + 1 + keep + 21 + pass + 1 + wan + out + inet + {{MIRROR_SYNC_PROTOCOL}} + {{WAN_IPADDR}} + {{MIRROR_UPSTREAM_NET}} + {{MIRROR_SYNC_PORT}} + D-107 controlled egress: per-DC mirror upstream sync only + + + 1 + keep + 99 + block + 1 + wan + out + inet + any + any + any + D-107 controlled egress: default-deny everything else outbound on WAN + + + + + + + + + diff --git a/scripts/opnsense-render-config.sh b/scripts/opnsense-render-config.sh new file mode 100644 index 0000000..2dd1ad8 --- /dev/null +++ b/scripts/opnsense-render-config.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# scripts/opnsense-render-config.sh +# +# Renders opentofu/templates/opnsense-config.xml.tmpl into a real config.xml +# by substituting {{TOKEN}} markers (this repo's existing clientdocs +# convention, reused here) from required environment variables -- no +# invented defaults except NTP_UPSTREAM_SERVERS/NTP_PREFER_SERVER, which +# default to the SAME public pool OPNsense's own shipped config.xml.sample +# uses (a real, confirmed default, not an invention -- see +# opentofu/templates/README.md). Everything else fails loud if unset. +# +# Feed the result to scripts/opnsense-build-config-iso.sh next. +# Exit: 0 rendered | 1 required token/template missing. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATE="$HERE/../opentofu/templates/opnsense-config.xml.tmpl" +OUT="${1:?usage: opnsense-render-config.sh }" + +: "${NTP_UPSTREAM_SERVERS:=0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org}" +: "${NTP_PREFER_SERVER:=0.opnsense.pool.ntp.org}" + +req() { # req VARNAME -- fail loud if unset, no inferred fallback + local name="$1" + [ -n "${!name:-}" ] || { echo "FAIL: \$$name not set -- see opentofu/templates/README.md"; exit 1; } +} + +REQUIRED_VARS=( + OPNSENSE_HOSTNAME DOMAIN ROOT_PASSWORD_HASH + WAN_IF WAN_IPADDR WAN_SUBNET_BITS WAN_GATEWAY + LAN_IF LAN_IPADDR LAN_SUBNET_BITS + MIRROR_SYNC_PROTOCOL MIRROR_UPSTREAM_NET MIRROR_SYNC_PORT +) +for v in "${REQUIRED_VARS[@]}"; do + req "$v" +done + +[ -r "$TEMPLATE" ] || { echo "FAIL: template not found: $TEMPLATE"; exit 1; } + +route_block() { # route_block + local net="${!1:-}" gw="${!2:-}" descr="${!3:-}" + if [ -z "$net" ]; then + return 0 # no route needed at this site -- emit nothing, not a placeholder + fi + [ -n "$gw" ] || { echo "FAIL: \$$1 is set but \$$2 is not"; exit 1; } + printf ' \n %s\n %s\n %s\n 1\n \n' \ + "$net" "$gw" "${descr:-static route}" +} + +ROUTE1_BLOCK="$(route_block ROUTE1_NETWORK ROUTE1_GATEWAY ROUTE1_DESCR)" +ROUTE2_BLOCK="$(route_block ROUTE2_NETWORK ROUTE2_GATEWAY ROUTE2_DESCR)" + +content="$(cat "$TEMPLATE")" +for v in "${REQUIRED_VARS[@]}" NTP_UPSTREAM_SERVERS NTP_PREFER_SERVER; do + content="${content//"{{$v}}"/${!v}}" +done +content="${content//"{{ROUTE1_BLOCK}}"/$ROUTE1_BLOCK}" +content="${content//"{{ROUTE2_BLOCK}}"/$ROUTE2_BLOCK}" + +if grep -qE '\{\{[A-Z0-9_]+\}\}' <<<"$content"; then + echo "FAIL: unresolved {{TOKEN}} remains in rendered output -- template/script token lists are out of sync:" + grep -oE '\{\{[A-Z0-9_]+\}\}' <<<"$content" | sort -u + exit 1 +fi + +printf '%s' "$content" > "$OUT" +echo "PASS: rendered $OUT" diff --git a/scripts/repo_lint.py b/scripts/repo_lint.py index 639fd02..ee80e75 100644 --- a/scripts/repo_lint.py +++ b/scripts/repo_lint.py @@ -174,7 +174,7 @@ return [p for p in out if p.is_file()] def all_text(): - exts = {".md", ".sh", ".py", ".yaml", ".yml", ".tf", ""} + exts = {".md", ".sh", ".py", ".yaml", ".yml", ".tf", ".tmpl", ""} skip_names = {"overrides.zip"} out = [] for p in R.rglob("*"): diff --git a/tests/opnsense-render-config/run-tests.sh b/tests/opnsense-render-config/run-tests.sh new file mode 100644 index 0000000..1c3ec8b --- /dev/null +++ b/tests/opnsense-render-config/run-tests.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# tests/opnsense-render-config/run-tests.sh -- offline harness for +# scripts/opnsense-render-config.sh. Unlike the other opnsense-* scripts, +# this one needs no external tool (pure bash + template substitution), so +# this harness tests the REAL behavior end-to-end, not just guard clauses. +# Exit: 0 all pass | 1 any case failed. ASCII + LF. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$HERE/../../scripts/opnsense-render-config.sh" +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +PASS=0; FAIL=0 + +base_env() { + export OPNSENSE_HOSTNAME="opnsense" DOMAIN="omega.dc1.vr1.cloud.neumatrix.local" + export ROOT_PASSWORD_HASH='$2y$10$fakehashfakehashfakehashfakehashfakehas' + export WAN_IF="vtnet0" WAN_IPADDR="203.0.113.2" WAN_SUBNET_BITS="30" WAN_GATEWAY="203.0.113.1" + export LAN_IF="vtnet1" LAN_IPADDR="10.12.8.1" LAN_SUBNET_BITS="22" + export MIRROR_SYNC_PROTOCOL="TCP" MIRROR_UPSTREAM_NET="10.12.5.10/32" MIRROR_SYNC_PORT="443" + unset ROUTE1_NETWORK ROUTE1_GATEWAY ROUTE1_DESCR ROUTE2_NETWORK ROUTE2_GATEWAY ROUTE2_DESCR + unset NTP_UPSTREAM_SERVERS NTP_PREFER_SERVER +} + +run_ok() { # run_ok