#!/usr/bin/env bash
# tests/dc-node-etchosts/run-tests.sh -- offline harness for scripts/dc-node-etchosts.sh
#
# No live juju. `render` is pure (sources lib-net). We grade three things:
# 1. render emits VALID cloud-init YAML with THIS DC's metal-internal CIDR (per-DC,
# derived from lib-net -- dc0 != dc1). A malformed cloudinit-userdata would break
# EVERY machine's provisioning, so YAML validity is a hard gate.
# 2. the embedded runcmd shell logic actually WORKS: it adds an /etc/hosts entry for
# an address IN the metal-internal CIDR, SKIPS one outside it (blast-radius), and
# is IDEMPOTENT. We run the real rendered command against a temp hosts file + a
# stub `hostname`, so behaviour is proven, not just "it parsed".
# 3. the render uses a JUJU-ACCEPTED key (postruncmd/preruncmd), NOT a bare `runcmd`
# which juju model-config rejects -- cloud-init accepts runcmd as valid YAML, so YAML
# validity alone cannot catch it (T10; the 2026-08-04 live-apply failure this harness
# was green through).
# 4. every assertion can FAIL (wrong CIDR, unscoped, non-idempotent, refuse, bare runcmd).
# ASCII + LF. Exit 0 all pass | 1 any fail.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
SCRIPT="$REPO/scripts/dc-node-etchosts.sh"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok(){ echo " PASS $1"; PASS=$((PASS+1)); }
no(){ echo " FAIL $1"; FAIL=$((FAIL+1)); }
# --- extract the runcmd shell script from a render, rewired to a temp hosts file ---
runcmd_of() { # <site> -> prints the /bin/bash -c script with /etc/hosts -> $TMP/hosts
bash "$SCRIPT" render "$1" | python3 -c "
import sys, yaml
d = yaml.safe_load(sys.stdin)
cmd = d['postruncmd'][0][2]
print(cmd.replace('/etc/hosts', '$TMP/hosts'))
"
}
# T1 render dc0 parses as cloud-init YAML with the postruncmd in the /bin/bash -c list form
if bash "$SCRIPT" render vr1-dc0 | python3 -c "
import sys, yaml
d = yaml.safe_load(sys.stdin)
rc = d['postruncmd'][0]
assert isinstance(rc, list) and rc[0:2]==['/bin/bash','-c'], 'postruncmd not /bin/bash -c list form'
assert d.get('postruncmd'), 'no postruncmd'
" 2>/dev/null; then ok "T1 render vr1-dc0 is valid cloud-init YAML (/bin/bash -c postruncmd)"; else no "T1 render vr1-dc0 valid YAML"; fi
# T2 dc0 carries dc0's metal-internal CIDR
bash "$SCRIPT" render vr1-dc0 | grep -q '10.12.12.0/22' && ok "T2 dc0 render pins metal-internal 10.12.12.0/22" || no "T2 dc0 CIDR"
# T3 PER-DC: dc1's metal-internal is 10.12.72.0/22 (lib-net; 10.12.68 is dc1 metal-ADMIN),
# NOT dc0's 10.12.12.0/22 -- proves the CIDR is derived from lib-net per site, keyed by
# PLANE_NAME=="metal-internal", not hardcoded or confused with metal-admin.
out1="$(bash "$SCRIPT" render vr1-dc1 2>/dev/null)"
if grep -q '10.12.72.0/22' <<<"$out1" && ! grep -q '10.12.12.0/22' <<<"$out1" && ! grep -q '10.12.68.0/22' <<<"$out1"; then
ok "T3 dc1 render pins metal-internal 10.12.72.0/22 (not dc0's, not dc1 metal-admin)"
else no "T3 dc1 per-DC CIDR (got: $(grep -oE '10\.12\.[0-9]+\.0/22' <<<"$out1" | tr '\n' ' '))"; fi
# T4 BEHAVIOUR: runcmd adds a metal-internal address, SKIPS a metal-admin address.
mkstubhost() { # <space-separated ips>
printf '#!/usr/bin/env bash\ncase "$1" in -I) echo "%s";; *) echo "node-x";; esac\n' "$1" > "$TMP/bin/hostname"
chmod +x "$TMP/bin/hostname"
}
mkdir -p "$TMP/bin"; : > "$TMP/hosts"
mkstubhost "10.12.12.50 10.12.8.60"
PATH="$TMP/bin:$PATH" bash -c "$(runcmd_of vr1-dc0)"
if grep -q '^10.12.12.50 ' "$TMP/hosts" && ! grep -q '10.12.8.60' "$TMP/hosts"; then
ok "T4 runcmd adds metal-internal 10.12.12.50, SKIPS metal-admin 10.12.8.60 (blast radius)"
else no "T4 scoping (hosts: $(cat "$TMP/hosts" | tr '\n' '|'))"; fi
# T5 IDEMPOTENT: running again does not duplicate the entry
PATH="$TMP/bin:$PATH" bash -c "$(runcmd_of vr1-dc0)"
n="$(grep -c '^10.12.12.50 ' "$TMP/hosts")"
[ "$n" = "1" ] && ok "T5 runcmd is idempotent (one entry after two runs)" || no "T5 idempotent (count=$n)"
# T6 the entry maps the address to a NON-EMPTY name (the CN must be non-empty)
grep -qE '^10.12.12.50 [^ ]+' "$TMP/hosts" && ok "T6 entry carries a non-empty hostname (valid CN source)" || no "T6 name present"
# T7 FAILING DIRECTION: an address OUTSIDE metal-internal alone yields NO entry.
: > "$TMP/hosts"; mkstubhost "10.12.8.99 10.12.4.99"
PATH="$TMP/bin:$PATH" bash -c "$(runcmd_of vr1-dc0)"
[ ! -s "$TMP/hosts" ] && ok "T7 no metal-internal address -> no entry (the guard can decline)" || no "T7 guard (hosts: $(cat "$TMP/hosts"))"
# T8 REFUSE on an unknown site
bash "$SCRIPT" render vr1-dc9 >/dev/null 2>&1; rc=$?
[ "$rc" -eq 2 ] && ok "T8 render REFUSES an unknown site (rc=2)" || no "T8 refuse unknown site (rc=$rc)"
# T9 REFUSE on a bad mode
bash "$SCRIPT" frobnicate vr1-dc0 >/dev/null 2>&1; rc=$?
[ "$rc" -eq 2 ] && ok "T9 REFUSES an unknown mode (rc=2)" || no "T9 refuse mode (rc=$rc)"
# T10 JUJU-ACCEPTANCE PROXY (the gap that let the live apply fail on 2026-08-04 while
# this harness was green): juju model-config REJECTS a top-level `runcmd` in
# cloudinit-userdata ("runcmd not allowed, use preruncmd or postruncmd instead"),
# so the render MUST use postruncmd/preruncmd and MUST NOT emit a bare `runcmd:`.
# cloud-init itself accepts runcmd as valid YAML, so YAML validity (T1) cannot catch
# this -- it is a juju-specific constraint. Proves the failing direction: revert the
# script to `runcmd:` and T10 goes red.
r10="$(bash "$SCRIPT" render vr1-dc0 2>/dev/null)"
if grep -qE '^(post|pre)runcmd:' <<<"$r10" && ! grep -qE '^runcmd:' <<<"$r10"; then
ok "T10 render uses juju-accepted postruncmd/preruncmd, not a bare runcmd (juju rejects runcmd)"
else no "T10 juju-accepted key (top-level keys: $(grep -oE '^[a-z]+runcmd:|^runcmd:' <<<"$r10" | tr '\n' ' '))"; fi
echo
echo "RESULT: PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] && { echo "ALL PASS"; exit 0; } || exit 1