Newer
Older
openstack-caracal-dc-dc / tests / roles-aggregates-import / run-tests.sh
#!/usr/bin/env bash
# tests/roles-aggregates-import/run-tests.sh
#
# Harness for netbox/roles-aggregates-import.py. OFFLINE -- touches no NetBox.
#
# THIS SCRIPT SHIPPED WITH NO HARNESS, and that is not incidental to the bug it
# carried. It was only ever exercised against an EMPTY NetBox. The moment it met
# one holding the real upstream draft (2026-07-13) it created 4 roles, hit a 400
# on the 5th, and DIED -- leaving the IPAM apex half-populated with no RIRs and
# no aggregates, and no rollback.
#
# Two properties are pinned here, both learned from that failure:
#   1. NetBox enforces UNIQUE ROLE NAMES, not just unique slugs. The legacy `repl`
#      role already holds the name "Replication", so the six-plane role (slug
#      `replication`) MUST NOT be named "Replication".
#   2. The importer must PREFLIGHT every name/slug before writing ANYTHING. An
#      IPAM importer that can die partway corrupts the thing it exists to populate.
# ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
S="$(cd "$HERE/../../netbox" && pwd)/roles-aggregates-import.py"
pass=0; fail=0
ok()  { pass=$((pass+1)); }
bad() { fail=$((fail+1)); echo "  FAIL: $1"; }

command -v python3 >/dev/null 2>&1 || { echo "FAIL: python3 required"; exit 1; }

python3 -c "import ast;ast.parse(open('$S').read())" 2>/dev/null && ok || bad "does not parse"
# This script needs pynetbox (unlike the stdlib tools). On a host without it, it
# must say so CLEANLY rather than traceback -- so accept either.
out="$(python3 "$S" --help 2>&1)"
if printf '%s' "$out" | grep -q "pynetbox not installed"; then ok      # clean, actionable
elif printf '%s' "$out" | grep -q -- "--commit"; then ok               # real help text
else bad "--help neither printed help nor a clean 'pynetbox not installed' message"; fi

# DRY BY DEFAULT -- this one already was, and must stay so.
grep -q -- '--commit' "$S" && ok || bad "no --commit flag"
grep -q 'default: preview only, no writes' "$S" && ok || bad "no longer documents preview-by-default"

# 1. THE NAME COLLISION. Naming the six-plane role "Replication" 400s against any
#    NetBox carrying the draft, because legacy slug `repl` holds that name.
grep -q '"Replication Plane"' "$S" && ok \
  || bad "the six-plane replication role is not named 'Replication Plane' -- it will 400 against the real draft (legacy slug 'repl' holds the name 'Replication')"
grep -qE '\("replication", "Replication",' "$S" \
  && bad "the six-plane role is named exactly 'Replication' -- COLLIDES with legacy slug 'repl'" || ok
# the slug must NOT drift: lib-net.sh SPACES6 and dc-dc-prefixes-import.py look it up.
grep -q '"replication", "Replication Plane"' "$S" && ok \
  || bad "the replication slug drifted -- lib-net.sh SPACES6 and the prefix importer look up slug 'replication'"

# 2. PREFLIGHT. Nothing may be written until the whole plan is known viable.
grep -q 'PREFLIGHT FAILED -- nothing was written' "$S" && ok \
  || bad "the preflight is gone -- a name collision can again leave the apex HALF-WRITTEN"
grep -q 'NetBox enforces UNIQUE NAMES, not just slugs' "$S" && ok \
  || bad "the preflight no longer checks NAME collisions (only slugs) -- that IS the bug"
# the preflight must run BEFORE the first create
pf=$(grep -n 'PREFLIGHT' "$S" | head -1 | cut -d: -f1)
cr=$(grep -n 'nb.ipam.roles.create' "$S" | head -1 | cut -d: -f1)
[ -n "$pf" ] && [ -n "$cr" ] && [ "$pf" -lt "$cr" ] && ok \
  || bad "preflight does not run BEFORE the first create -- half-written apex is back"

# 2026-07-14: EVERY die() downstream of the first write must be COVERED by the
# preflight. The old harness asserted only that the WORD "PREFLIGHT" appeared before
# the first create -- so it went green while the ARIN lookup and the ORG_ULA_48
# validation still ran AFTER 5 roles had been committed. A typo'd ULA, or an apex
# with no ARIN, left the PRODUCTION IPAM half-populated with no rollback. That is
# the very failure this preflight exists to prevent, still armed one section lower.
# Assert by POSITION, not by vocabulary.
arin=$(grep -n "rirs.get(slug=\"arin\")" "$S" | head -1 | cut -d: -f1)
[ -n "$arin" ] && [ "$arin" -lt "$cr" ] && ok \
  || bad "the ARIN RIR check runs AFTER the first role create -- a missing ARIN half-writes the apex"
ula=$(grep -n 'validate_ula_48(' "$S" | sed -n '2p' | cut -d: -f1)   # [1] is the def
[ -n "$ula" ] && [ "$ula" -lt "$cr" ] && ok \
  || bad "ORG_ULA_48 is validated AFTER the first role create -- a typo half-writes the apex"
grep -q 'a typo must not cost a half-populated apex' "$S" && ok \
  || bad "the ULA preflight rationale is gone -- someone will move it back below the writes"

# WAF trap: get_nb must set the accepted User-Agent on the pynetbox session, or
# every upstream call 403s (looks like auth failure; references/platform-traps.md).
grep -q 'http_session.headers\["User-Agent"\] = "curl/8.5.0"' "$S" && ok \
  || bad "get_nb does not set the WAF-safe User-Agent -- upstream writes will 403"

# UPSTREAM-WRITE GUARD: --commit to a non-sandbox host must require --yes-write-upstream.
# The WAF 403 used to be the only accidental-safety; the UA fix removed it.
grep -q -- '--yes-write-upstream' "$S" && ok \
  || bad "no --yes-write-upstream flag -- an accidental --commit can hit the production apex"
grep -q 'SANDBOX_HOSTS' "$S" && ok \
  || bad "no SANDBOX_HOSTS gate -- the upstream-write guard is gone"
grep -q 'REFUSING to --commit' "$S" && ok \
  || bad "the upstream-write refusal message is gone"
# the guard must gate on args.yes_write_upstream before writing
grep -q 'not args.yes_write_upstream' "$S" && ok \
  || bad "the guard does not actually check the --yes-write-upstream flag"

# 3. the six-plane slugs must match lib-net.sh SPACES6 exactly.
for slug in provider-public metal-admin metal-internal data-tenant replication; do
  grep -q "\"$slug\"" "$S" && ok || bad "six-plane role slug '$slug' is gone"
done
# storage is deliberately NOT created -- it pre-exists upstream and is reused.
grep -q 'already exists -- intentionally omitted' "$S" && ok \
  || bad "the note that 'storage' is reused (not created) is gone"

echo
total=$((pass+fail))
if [ "$fail" -eq 0 ]; then echo "roles-aggregates-import: $pass/$total PASS"; exit 0; fi
echo "roles-aggregates-import: $fail/$total FAIL"; exit 1