#!/usr/bin/env bash
# scripts/opentofu-validate.sh [--static-only] [tofu-dir]
#
# Syntax/schema/semantic gate for opentofu/ (VR1 IaC, D-103). Read-only: runs the
# static semantic guards (S1, S2) + `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.
#
# The S* guards catch libvirt-domain defects that `tofu validate` structurally
# CANNOT see (optional attributes whose ABSENCE is the bug). Both were paid for in
# blood by the 2026-07-12 Office1 OPNsense boot incident:
# S1 memory without memory_unit -> 1024x too little RAM (guest triple-faults)
# S2 domain without ACPI enabled -> FreeBSD panics; Linux loses clean shutdown
#
# --static-only runs ONLY the S* guards and exits (no tofu binary, no network
# needed). That is how the harness exercises them cheaply.
#
# Exit: 0 clean | 1 S*/fmt/init/validate failed | 2 tofu binary not found.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODE="full"
if [ "${1:-}" = "--static-only" ] || [ "${1:-}" = "--check-memory-unit" ]; then
MODE="static" # --check-memory-unit kept as a back-compat alias
shift
fi
TOFU_DIR="${1:-$HERE/../opentofu}"
if [ ! -d "$TOFU_DIR" ]; then
echo "FAIL: no such directory: $TOFU_DIR"
exit 1
fi
# S1: every libvirt_domain that sets `memory` MUST also set `memory_unit`.
#
# `tofu validate` CANNOT catch this: memory_unit is an optional attribute, so
# omitting it is schema-valid. But on dmacvicar/libvirt >=0.9 a bare `memory` is
# interpreted in libvirt's DEFAULT unit (KiB), not MiB (the 0.8-era meaning), so
# `memory = 2048` silently yields a 2 MiB guest that triple-faults in its
# bootloader. That cost a full session on 2026-07-12 (Office1 OPNsense edge; see
# docs/incident-20260712-opnsense-edge-boot-triplefault.md). This is the guard
# that stops the next VM module from reintroducing it.
#
# Block-scan is safe because `tofu fmt -check` (below) enforces canonical layout:
# top-level resource braces at column 0, attributes at two-space indent. Run
# order puts S1 first so it also works with no tofu binary present.
s1_check() {
local out
out="$(find "$TOFU_DIR" -name '*.tf' -not -path '*/.terraform/*' -print0 2>/dev/null \
| xargs -0 -r awk '
/^resource "libvirt_domain"/ { inblk=1; mem=0; unit=0; rline=FNR; next }
inblk && /^ memory[[:space:]]*=/ { mem=1 }
inblk && /^ memory_unit[[:space:]]*=/ { unit=1 }
inblk && /^}/ {
if (mem && !unit)
printf " [FAIL] %s:%d sets memory with no memory_unit -- guest gets KiB, not MiB (1024x too little RAM)\n", FILENAME, rline
inblk=0
}
')"
if [ -n "$out" ]; then
echo "$out"
echo ' FIX: add memory_unit = "MiB" to each domain listed above.'
return 1
fi
echo " [PASS] every libvirt_domain that sets memory also sets memory_unit"
return 0
}
# S2: every libvirt_domain MUST enable ACPI (`features = { acpi = true }`).
#
# Also invisible to `tofu validate` (the whole `features` block is optional). With no
# features block libvirt renders the machine `acpi=off`. A FreeBSD/OPNsense guest then
# PANICS at interrupt init ("running without device atpic requires a local APIC") and
# spins in ddb at 100% CPU while looking "running". A Linux guest boots but loses clean
# ACPI shutdown -- which breaks MAAS power control on the node VMs. Cost: the second half
# of the 2026-07-12 boot incident, found only once the S1 fix let the kernel get far
# enough to reach interrupt init.
s2_check() {
local out
out="$(find "$TOFU_DIR" -name '*.tf' -not -path '*/.terraform/*' -print0 2>/dev/null \
| xargs -0 -r awk '
/^resource "libvirt_domain"/ { inblk=1; acpi=0; rline=FNR; next }
inblk && /^[[:space:]]*acpi[[:space:]]*=[[:space:]]*true/ { acpi=1 }
inblk && /^}/ {
if (!acpi)
printf " [FAIL] %s:%d libvirt_domain does not enable ACPI -- libvirt renders acpi=off; FreeBSD guests panic, Linux guests lose clean shutdown\n", FILENAME, rline
inblk=0
}
')"
if [ -n "$out" ]; then
echo "$out"
echo ' FIX: add features = { acpi = true, apic = {} } to each domain listed above.'
return 1
fi
echo " [PASS] every libvirt_domain enables ACPI"
return 0
}
echo "== S1 libvirt_domain memory_unit guard =="
if s1_check; then
s1_rc=0
else
s1_rc=1
fi
echo "== S2 libvirt_domain ACPI guard =="
if s2_check; then
s2_rc=0
else
s2_rc=1
fi
if [ "$MODE" = "static" ]; then
[ "$s1_rc" -eq 0 ] && [ "$s2_rc" -eq 0 ] && exit 0
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=$(( s1_rc | s2_rc ))
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