#!/usr/bin/env bash
# tests/pre-flight-checks/run-tests.sh -- offline harness for scripts/pre-flight-checks.sh
# (preflight's P4 gate).
#
# WHY IT EXISTS. Until 2026-07-29 this script had NO harness at all, and the only place
# it had ever been observed running was a live jumphost where it FAILED -- eleven fatals,
# of which zero described the cloud. That is the repo's own inverse rule biting: "a
# checker that cannot fail is not a gate" has a twin, and a gate only ever observed
# FAILING needs a constructed fixture proving it can go green. So T1/T2/T3 build a
# CORRECT DC and require exit 0, and everything after them flips exactly one field of
# that same fixture and requires the gate to go red on the assertion that field feeds.
#
# HERMETIC: a fake `maas` on PATH serves JSON from files in the fixture; nothing reaches
# a real MAAS, a real cloud or the real repo. Fixtures are BUILT FROM lib-net.sh +
# lib-hosts.sh for the selected DC, so a green case attests that the gate agrees with the
# libraries rather than with a second hand-typed copy of the same numbers.
#
# Exit: 0 all pass | 1 any case failed. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok() { echo " PASS $1"; PASS=$((PASS+1)); }
bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); }
command -v jq >/dev/null 2>&1 || { echo " FAIL jq is required by the script under test"; echo "RESULT: PASS=0 FAIL=1"; exit 1; }
# ---------------------------------------------------------------------------
# mkfix <name> <dc> -> echoes the fixture repo path
# Builds a repo whose MAAS state is exactly what the selected DC's libraries describe.
mkfix() {
local name="$1" dc="$2" d="$TMP/$1"
mkdir -p "$d/scripts" "$d/overlays" "$d/fakebin" "$d/maasdata"
cp "$REPO/scripts/pre-flight-checks.sh" "$REPO/scripts/lib-net.sh" \
"$REPO/scripts/lib-hosts.sh" "$d/scripts/"
printf 'applications:\n keystone:\n charm: keystone\n' > "$d/bundle.yaml"
# The octavia-pki overlay is a gitignored SECRET in the real tree, so the fixture
# mints its own with the 5 lb-mgmt-* keys the gate counts. No key material: the
# values are placeholders and the gate never prints them anyway.
{
printf 'applications:\n octavia:\n options:\n'
for k in lb-mgmt-issuing-cacert lb-mgmt-issuing-ca-private-key \
lb-mgmt-issuing-ca-key-passphrase lb-mgmt-controller-cacert \
lb-mgmt-controller-cert; do
printf ' %s: PLACEHOLDER\n' "$k"
done
} > "$d/overlays/${dc}-octavia-pki.yaml"
# Dump the DC's own view of planes and hosts. A SUBSHELL per dump: both libs refuse a
# second, different selection in the same shell.
( . "$REPO/scripts/lib-net.sh"; lib_net_select_dc "$dc" >/dev/null
for c in "${PLANE_CIDRS[@]}"; do printf 'P\t%s\t%s\t%s\n' "$c" "${PLANE_NAME[$c]}" "${PLANE_GW[$c]:-}"; done
for c in "${DATA_PLANE_CIDRS[@]}"; do printf 'D\t%s\n' "$c"; done
printf 'M\t%s\n' "$METAL_INTERNAL_CIDR"
printf 'I\t%s\n' "${METAL_INTERNAL_IFACE:-}"
printf 'T\t%s\n' "${METAL_INTERNAL_VID:-0}"
printf 'V\t%s\t%s\t%s\t%s\t%s\n' "$VIP_PREFIX_PROVIDER" "$VIP_PREFIX_ADMIN" \
"$VIP_PREFIX_INTERNAL" "$VIP_OCTET_MIN" "$VIP_COUNT_EXPECT"
) > "$d/maasdata/planes.tsv"
( . "$REPO/scripts/lib-hosts.sh"; lib_hosts_select_dc "$dc" >/dev/null
for h in "${HOSTS[@]}"; do printf 'H\t%s\t%s\t%s\n' "$h" "${HOST_OCTET[$h]}" "${HOST_BOOT_MAC[$h]}"; done
) > "$d/maasdata/hosts.tsv"
D="$d" python3 - <<'PY'
import json, os
d = os.environ["D"]
planes, data, hosts = [], [], []
mi = mi_iface = ""
mi_vid = 0
vip = None
for line in open(d + "/maasdata/planes.tsv"):
f = line.rstrip("\n").split("\t")
if f[0] == "P": planes.append((f[1], f[2], f[3]))
elif f[0] == "D": data.append(f[1])
elif f[0] == "M": mi = f[1]
elif f[0] == "I": mi_iface = f[1]
elif f[0] == "T": mi_vid = int(f[1] or 0)
elif f[0] == "V": vip = f[1:]
for line in open(d + "/maasdata/hosts.tsv"):
f = line.rstrip("\n").split("\t")
if f[0] == "H": hosts.append((f[1], int(f[2]), f[3]))
subnets = []
for i, (cidr, name, gw) in enumerate(planes):
# VR0's metal-internal is the tagged VLAN-103 stack; every VR1 plane is untagged
# (D-133 flat carve), and MAAS renders the untagged/default VLAN as vid 0.
subnets.append({"id": 100 + i, "cidr": cidr, "name": name,
"vlan": {"vid": mi_vid if cidr == mi else 0},
"gateway_ip": gw or None, "dns_servers": []})
json.dump(subnets, open(d + "/maasdata/subnets.json", "w"), indent=1)
# The D-134 utility band (.4-.49) is per-DC infrastructure, not an OpenStack role node.
# VR0's .40-.43 index predates those bands, so vr0-dc0's fleet is all role nodes.
vr0 = any(h[0].startswith("openstack") for h in hosts)
machines = []
for n, (host, octet, mac) in enumerate(hosts):
role = vr0 or octet >= 100
sid = "sysid%02d" % n
# A DELIBERATELY random MAAS hostname for VR1: lib-hosts' VR1 HOSTS entries are tofu
# node names, and MAAS re-mints its own name on every re-enlistment. A gate that
# resolves by hostname cannot pass this fixture.
maas_hostname = host if vr0 else "maas-random-%02d" % n
machines.append({"system_id": sid, "hostname": maas_hostname,
# role nodes are handed off Ready (DOCFIX-200); the D-104 Juju
# controller is legitimately Deployed once bootstrapped.
"status_name": "Ready" if role else "Deployed",
"power_state": "off",
"boot_interface": {"mac_address": mac}})
ifaces = []
if not vr0:
ifaces.append({"name": "enp1s0", "type": "physical",
"links": [{"subnet": {"cidr": planes[1][0]},
"ip_address": planes[1][0].split("/")[0].rsplit(".", 1)[0] + ".%d" % octet}]})
ifaces.append({"name": "enp2s0", "type": "physical", "links": []})
for j, cidr in enumerate(data):
base = cidr.split("/")[0].rsplit(".", 1)[0]
if cidr == mi and mi_iface:
nm, ty = mi_iface, "bridge" # VR0 Pattern A: the br-internal stack
else:
nm, ty = "enp%ds0" % (3 + j), "physical" # D-133 flat carve
ifaces.append({"name": nm, "type": ty,
"links": [{"subnet": {"cidr": cidr}, "ip_address": "%s.%d" % (base, octet)}]})
json.dump(ifaces, open(d + "/maasdata/if-%s.json" % sid, "w"), indent=1)
json.dump(machines, open(d + "/maasdata/machines.json", "w"), indent=1)
# A VIP overlay for THIS DC, built from the DC's own bands so a green run attests the
# gate and lib-net agree. Column order provider / metal-admin / metal-internal.
pp, pa, pi, lo, cnt = vip[0], vip[1], vip[2], int(vip[3]), int(vip[4])
with open(d + "/overlays/%s-vips.yaml" % os.environ["DCNAME"], "w") as fh:
fh.write("applications:\n")
for k in range(cnt):
o = lo + k
fh.write(" app%02d:\n options:\n vip: \"%s.%d %s.%d %s.%d\"\n" % (k, pp, o, pa, o, pi, o))
PY
echo "$d"
}
# The python block needs the DC name for the overlay filename.
mkfix_dc() { DCNAME="$2" mkfix "$@"; }
# fakebin maas: serves the fixture JSON. Nothing else is answerable, and an
# unrecognised call exits non-zero rather than printing an empty success.
mkmaas() {
local d="$1"
cat > "$d/fakebin/maas" <<'FB'
#!/usr/bin/env bash
DD="$(dirname "$(dirname "$0")")/maasdata"
case "$2 $3" in
"subnets read") cat "$DD/subnets.json" ;;
"machines read") cat "$DD/machines.json" ;;
"interfaces read") cat "$DD/if-$4.json" 2>/dev/null || echo "[]" ;;
*) echo "fake maas: unhandled '$*'" >&2; exit 2 ;;
esac
FB
chmod +x "$d/fakebin/maas"
}
# run <dir> <dc> <want_rc> <regex-or-"-"> <label>
run() {
local d="$1" dc="$2" want="$3" rx="$4" label="$5" out rc
out="$(PATH="$d/fakebin:$PATH" DC="$dc" bash "$d/scripts/pre-flight-checks.sh" 2>&1)"; rc=$?
local hit=1
[ "$rx" = "-" ] || { grep -qE "$rx" <<<"$out"; hit=$?; }
if [ "$rc" = "$want" ] && [ "$hit" -eq 0 ]; then
ok "$label"
else
bad "$label (rc=$rc want=$want)"
printf '%s\n' "$out" | grep -E '^(FAIL|WARN|Summary)' | head -6 | sed 's/^/ /'
fi
}
# jqedit <dir> <file> <jq-program> -- rewrite one fixture file in place
jqedit() { local f="$1/maasdata/$2"; jq "$3" "$f" > "$f.new" && mv "$f.new" "$f"; }
echo "== pre-flight-checks: the gate can go GREEN on a correct DC =="
D1="$(mkfix_dc green_dc1 vr1-dc1)"; mkmaas "$D1"
run "$D1" vr1-dc1 0 'Summary: 0 fatal, 0 warning' "T1 vr1-dc1 correct fixture -> exit 0"
D0="$(mkfix_dc green_dc0 vr1-dc0)"; mkmaas "$D0"
run "$D0" vr1-dc0 0 'Summary: 0 fatal, 0 warning' "T2 vr1-dc0 correct fixture -> exit 0"
DV="$(mkfix_dc green_vr0 vr0-dc0)"; mkmaas "$DV"
run "$DV" vr0-dc0 0 'Summary: 0 fatal, 0 warning' "T3 vr0-dc0 correct fixture -> exit 0 (VR0 arm still works)"
echo "== it is actually DC-AWARE, not DC-blind =="
# The single most important negative: point a dc0 run at dc1's MAAS and it must NOT pass.
# Before 2026-07-29 this script never read DC, so both invocations were byte-identical.
run "$D1" vr1-dc0 1 'plane metal-admin \(10.12.8.0/22\) NOT FOUND' \
"T4 DC=vr1-dc0 against dc1's MAAS -> FAILS (planes are per-DC)"
run "$D0" vr1-dc1 1 'plane metal-admin \(10.12.68.0/22\) NOT FOUND' \
"T5 DC=vr1-dc1 against dc0's MAAS -> FAILS (planes are per-DC)"
run "$D1" vr1-dc1 0 'gating DC=vr1-dc1' "T6 the header names the DC it measured"
run "$D1" bogus-dc 1 'refused DC' "T7 unknown DC token REFUSES (no default-to-success)"
run "$D1" dc1 1 'RETIRED' "T8 bare 'dc1' is rejected as RETIRED (D-119)"
echo "== item 3.7: the metal-internal expectations are D-133-correct, and still falsifiable =="
DT="$(mkfix_dc vid_tagged vr1-dc1)"; mkmaas "$DT"
jqedit "$DT" subnets.json '(.[] | select(.cidr=="10.12.72.0/22") | .vlan.vid) = 103'
run "$DT" vr1-dc1 1 'D-133 rules it UNTAGGED' "T9 a VLAN tag on VR1 metal-internal -> FAIL (abolished stack returning)"
DN="$(mkfix_dc vid_unreadable vr1-dc1)"; mkmaas "$DN"
jqedit "$DN" subnets.json '(.[] | select(.cidr=="10.12.72.0/22") | .vlan) = null'
run "$DN" vr1-dc1 1 'NO readable VLAN' "T10 an unreadable VLAN REFUSES (does not read as untagged)"
DB="$(mkfix_dc mi_bridged vr1-dc1)"; mkmaas "$DB"
jqedit "$DB" if-sysid00.json '(.[] | select(.links[0].subnet.cidr=="10.12.72.0/22") | .type) = "bridge"'
run "$DB" vr1-dc1 1 'D-133 rules a FLAT carve' "T11 a bridge over VR1 metal-internal -> FAIL (flat-carve invariant)"
DVV="$(mkfix_dc vr0_vid_wrong vr0-dc0)"; mkmaas "$DVV"
jqedit "$DVV" subnets.json '(.[] | select(.cidr=="10.12.12.0/22") | .vlan.vid) = 0'
run "$DVV" vr0-dc0 1 'want 103' "T12 VR0 still asserts VID 103 (the guard did not delete the VR0 branch)"
echo "== the retired metal-admin gateway expectation =="
# lib-net's VR1 arms no longer pin 10.12.8.1 / 10.12.68.1: measured 2026-07-27, MAAS
# carries no gateway there and the address answers nothing (D-134 rules .1 gateways for
# the PROVIDER subnets only). T1/T2 above are the proof that gateway=none now PASSES.
# This case is the other half: the expectation was retired, not deleted -- an
# unexplained gateway appearing on metal-admin is still reported.
DG="$(mkfix_dc spurious_gw vr1-dc1)"; mkmaas "$DG"
jqedit "$DG" subnets.json '(.[] | select(.cidr=="10.12.68.0/22") | .gateway_ip) = "10.12.68.1"'
run "$DG" vr1-dc1 2 'metal-admin has gateway 10.12.68.1' "T13 a spurious metal-admin gateway is still reported (WARN)"
DGP="$(mkfix_dc missing_gw vr1-dc1)"; mkmaas "$DGP"
jqedit "$DGP" subnets.json '(.[] | select(.cidr=="10.12.64.0/22") | .gateway_ip) = null'
run "$DGP" vr1-dc1 1 'provider-public gateway=none' "T14 a MISSING provider-public gateway still FAILS (ruled edge LAN gw)"
echo "== node identity: VR1 resolves by pinned boot MAC, never by MAAS hostname =="
DM="$(mkfix_dc mac_gone vr1-dc1)"; mkmaas "$DM"
jqedit "$DM" machines.json '(.[] | select(.system_id=="sysid00") | .boot_interface.mac_address) = "52:54:00:de:ad:00"'
run "$DM" vr1-dc1 1 'vr1-dc1-control-01 not enrolled in MAAS' "T15 a role node whose pinned MAC is absent -> FAIL"
DS="$(mkfix_dc not_ready vr1-dc1)"; mkmaas "$DS"
jqedit "$DS" machines.json '(.[] | select(.system_id=="sysid03") | .status_name) = "Broken"'
run "$DS" vr1-dc1 1 'status=Broken \(want Ready\)' "T16 a role node not Ready -> FAIL (DOCFIX-200 hand-off state)"
DL="$(mkfix_dc link_gone vr1-dc1)"; mkmaas "$DL"
jqedit "$DL" if-sysid05.json '[ .[] | select(.links[0].subnet.cidr != "10.12.80.0/22") ]'
run "$DL" vr1-dc1 1 'missing link on storage' "T17 a role node missing a data-plane link -> FAIL"
DO="$(mkfix_dc bad_octet vr1-dc1)"; mkmaas "$DO"
jqedit "$DO" if-sysid01.json '(.[] | select(.links[0].subnet.cidr=="10.12.76.0/22") | .links[0].ip_address) = "10.12.76.199"'
run "$DO" vr1-dc1 2 'want last octet .101' "T18 a wrong D-134 octet -> WARN"
echo "== the D-104 Juju controller is classified, not silently skipped =="
run "$D1" vr1-dc1 0 'utility-band infrastructure, not an OpenStack role node' \
"T19 the .5 utility node is NAMED and excluded with its reason"
# ... and its own record is still asserted: it is a pinned node of this DC.
DJ="$(mkfix_dc juju_gone vr1-dc1)"; mkmaas "$DJ"
jqedit "$DJ" machines.json '[ .[] | select(.system_id != "sysid09") ]'
run "$DJ" vr1-dc1 1 'utility band .5\) has NO MAAS record' "T20 the utility node vanishing still FAILS"
# It must NOT be asserted Ready: it is legitimately Deployed once bootstrapped, and the
# green fixture above already carries status Deployed for it. T1 passing IS that proof;
# this pins the reason so a later edit cannot re-add the assertion unnoticed.
if PATH="$D1/fakebin:$PATH" DC=vr1-dc1 bash "$D1/scripts/pre-flight-checks.sh" 2>&1 \
| grep -q 'juju-01 status='; then
bad "T21 the bootstrapped controller must NOT be asserted Ready"
else
ok "T21 the bootstrapped controller (status Deployed) is not asserted Ready"
fi
echo "== CHECK 0 / CHECK 1 are per-DC artifacts =="
DP="$(mkfix_dc pki_gone vr1-dc1)"; mkmaas "$DP"; rm -f "$DP/overlays/vr1-dc1-octavia-pki.yaml"
run "$DP" vr1-dc1 1 'MISSING overlays/vr1-dc1-octavia-pki.yaml' "T22 CHECK 0 names the PER-DC overlay (F1)"
DK="$(mkfix_dc pki_short vr1-dc1)"; mkmaas "$DK"
printf 'applications:\n octavia:\n options:\n lb-mgmt-controller-cert: X\n' \
> "$DK/overlays/vr1-dc1-octavia-pki.yaml"
run "$DK" vr1-dc1 1 'lb-mgmt-\* keys \(want 5\)' "T23 CHECK 0 asserts CONTENT, not existence"
# The inherit_errexit trap this repo has hit three times: a bare $( ) whose pipeline
# fails ABORTS instead of reaching the refusal branch. bundle.yaml is VIP-FREE, so with
# no per-DC overlay the vip grep matches NOTHING -- the normal case here, not an edge
# one. The gate must REFUSE with a message AND still reach its summary.
DW="$(mkfix_dc vips_gone vr1-dc1)"; mkmaas "$DW"; rm -f "$DW/overlays/vr1-dc1-vips.yaml"
run "$DW" vr1-dc1 1 'no VIP overlay for DC=vr1-dc1' "T24 a missing per-DC VIP overlay REFUSES with a message"
run "$DW" vr1-dc1 1 'Summary: [0-9]+ fatal' "T25 ... and the run still reaches its summary (no silent abort)"
DX="$(mkfix_dc vips_wrong vr1-dc1)"; mkmaas "$DX"
sed -i 's/10\.12\.68\.50/10.12.99.50/' "$DX/overlays/vr1-dc1-vips.yaml"
run "$DX" vr1-dc1 1 'WRONG admin col' "T26 a VIP outside this DC's admin band -> FAIL"
# The REAL committed artifact must pass too: a fixture-only green would prove nothing
# about the file the deploy actually consumes (R2 dual-family sextets, 13 apps).
DR="$(mkfix_dc real_overlay vr1-dc1)"; mkmaas "$DR"
cp "$REPO/overlays/vr1-dc1-vips.yaml" "$DR/overlays/vr1-dc1-vips.yaml"
run "$DR" vr1-dc1 0 'Summary: 0 fatal, 0 warning' "T27 the REAL committed vr1-dc1 VIP overlay passes CHECK 1"
# ... and the dc0 twin, which matters MORE: vr1-dc0 is the DEFAULT DC, so this is the
# artifact preflight feeds its gates when nobody sets DC at all.
DR0="$(mkfix_dc real_overlay_dc0 vr1-dc0)"; mkmaas "$DR0"
cp "$REPO/overlays/vr1-dc0-vips.yaml" "$DR0/overlays/vr1-dc0-vips.yaml"
run "$DR0" vr1-dc0 0 'Summary: 0 fatal, 0 warning' "T28 the REAL committed vr1-dc0 VIP overlay passes CHECK 1 (default DC)"
echo "== an octet in NO ruled D-134 band REFUSES, once =="
# The classification pass runs ONCE and stores its verdict, because CHECK 2 and CHECK 4
# both need it: deriving it per-check emitted the refusal twice and then printed a
# "utility-band" note contradicting the refusal that had just fired.
DU="$(mkfix_dc unclassified vr1-dc1)"; mkmaas "$DU"
sed -i 's/\[vr1-dc1-compute-02\]=121/[vr1-dc1-compute-02]=60/' "$DU/scripts/lib-hosts.sh"
run "$DU" vr1-dc1 1 'octet .60 is in NO ruled D-134 band' "T29 an out-of-band octet REFUSES"
NREF="$(PATH="$DU/fakebin:$PATH" DC=vr1-dc1 bash "$DU/scripts/pre-flight-checks.sh" 2>&1 \
| grep -c 'is in NO ruled D-134 band')"
if [ "$NREF" = "1" ]; then ok "T30 ... exactly once (classified once, not per check)"
else bad "T30 the out-of-band refusal fired $NREF time(s), want 1"; fi
if PATH="$DU/fakebin:$PATH" DC=vr1-dc1 bash "$DU/scripts/pre-flight-checks.sh" 2>&1 \
| grep -q 'vr1-dc1-compute-02 (.*utility-band infrastructure'; then
bad "T31 a refused node must not also be reported as utility-band"
else
ok "T31 a refused node is not also reported as utility-band"
fi
echo
echo "RESULT: PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] && { echo "ALL PASS"; exit 0; } || exit 1