#!/usr/bin/env python3
"""
Behavioral tests for netbox/dc-edge-wan-import.py -- drives the REAL main()/preflight
against an in-memory fake NB (tests/dc-edge-wan-import/fake_netbox.py) injected at the
get_nb() seam. No live NetBox. Invoked by run-tests.sh alongside the static greps.
The properties pinned here map to this repo's documented IPAM bug classes:
* HALF-WRITE (roles-aggregates-import.py): a later target's missing precondition
must abort with ZERO creates, never "wrote dc0 then died".
* SWAPPED SCOPE (the D-117 wrong-target near-miss): the vr1-dc0 /24 must bind
vr1-dc0's id and the vr1-dc1 /24 must bind vr1-dc1's id -- not shared or swapped.
* ALREADY-PRESENT = SKIP (idempotent), distinct from missing-infra = die.
* DRY BY DEFAULT: no --commit writes NOTHING.
* UPSTREAM-WRITE guard: --commit to a non-sandbox host refuses without the flag.
"""
from __future__ import annotations
import contextlib
import faulthandler
import importlib.util
import inspect
import io
import os
import sys
faulthandler.dump_traceback_later(30, exit=True)
HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(os.path.dirname(HERE))
TARGET_PATH = os.path.join(REPO_ROOT, "netbox", "dc-edge-wan-import.py")
sys.path.insert(0, HERE)
import fake_netbox # noqa: E402
P = 0
F = 0
def ok(label):
global P
P += 1
print(f"PASS: {label}")
def no(label, detail=""):
global F
F += 1
print(f"FAIL: {label}" + (f" ({detail})" if detail else ""))
def check(cond, label, detail=""):
ok(label) if cond else no(label, detail)
def load_target():
spec = importlib.util.spec_from_file_location("dc_edge_wan_import", TARGET_PATH)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
T = load_target()
@contextlib.contextmanager
def captured_stdout():
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
yield buf
def run_main(argv, fake):
T.get_nb = lambda base=None, token=None: fake
old = sys.argv
sys.argv = ["dc-edge-wan-import.py"] + argv
try:
return T.main()
finally:
sys.argv = old
def run_dies(label, argv, fake):
# swallow stdout AND stderr -- die() prints "FAIL: ..." to stderr by design, and
# for an EXPECTED death that is not a test failure, just noise on the gauntlet.
try:
with contextlib.redirect_stderr(io.StringIO()), captured_stdout():
run_main(argv, fake)
no(label, "did not raise SystemExit")
except SystemExit:
ok(label)
# Full-precondition fixture builders (role + container + both DC sites present).
ROLE = {"slug": "edge", "name": "Edge", "id": 1}
CONTAINER = {"prefix": "172.30.0.0/16", "id": 10}
SITE_DC0 = {"slug": "vr1-dc0", "name": "VR1 DC0", "id": 20}
SITE_DC1 = {"slug": "vr1-dc1", "name": "VR1 DC1", "id": 21}
def full_fake(prefixes_extra=()):
return fake_netbox.FakeNB(
roles=[ROLE],
prefixes=[CONTAINER, *prefixes_extra],
sites=[SITE_DC0, SITE_DC1],
)
os.environ["NETBOX_URL"] = "http://10.10.1.10:8000" # a known sandbox (guard passes)
os.environ["NETBOX_TOKEN"] = "fake-token"
# -----------------------------------------------------------------------------
# 1. DRY RUN is the default and writes NOTHING.
# -----------------------------------------------------------------------------
fk = full_fake()
with captured_stdout() as out:
rc = run_main([], fk)
check(rc == 0, "dry run rc==0", str(rc))
check(out.getvalue().count("would CREATE") == 2, "dry run PLANS both /24s",
str(out.getvalue().count("would CREATE")))
check(out.getvalue().count("CREATED ") == 0, "dry run CREATES nothing (no CREATED line)")
check("DRY RUN" in out.getvalue(), "dry run says so, loudly")
check(len(fk.creates) == 0, "dry run left the NetBox untouched (0 writes)", str(len(fk.creates)))
# -----------------------------------------------------------------------------
# 2. --commit writes exactly the two /24s, correctly scoped (NO swap).
# -----------------------------------------------------------------------------
fk = full_fake()
with captured_stdout() as out:
rc = run_main(["--commit"], fk)
check(rc == 0, "commit rc==0", str(rc))
check(len(fk.creates) == 2, "commit created exactly 2 prefixes", str(len(fk.creates)))
check(all(path == "ipam/prefixes" for path, _ in fk.creates),
"commit wrote only ipam/prefixes (no stray sites/roles/container)")
by_prefix = {pl["prefix"]: pl for _, pl in fk.creates}
check(by_prefix.get("172.30.2.0/24", {}).get("scope_id") == 20,
"SCOPE: 172.30.2.0/24 binds vr1-dc0's id (20)", str(by_prefix.get("172.30.2.0/24")))
check(by_prefix.get("172.30.3.0/24", {}).get("scope_id") == 21,
"SCOPE: 172.30.3.0/24 binds vr1-dc1's id (21) -- not swapped",
str(by_prefix.get("172.30.3.0/24")))
check(all(pl.get("scope_type") == "dcim.site" for pl in by_prefix.values()),
"SCOPE: both /24s are dcim.site-scoped (matches office1-wan)")
check(all(pl.get("role") == 1 for pl in by_prefix.values()), "both /24s carry the edge role id")
check(all(pl.get("status") == "active" for pl in by_prefix.values()), "both /24s are status active")
# -----------------------------------------------------------------------------
# 3. Already-present = idempotent SKIP (both present -> 0 creates, exit clean).
# -----------------------------------------------------------------------------
fk = full_fake(prefixes_extra=[{"prefix": "172.30.2.0/24", "id": 30},
{"prefix": "172.30.3.0/24", "id": 31}])
with captured_stdout() as out:
rc = run_main(["--commit"], fk)
check(rc == 0, "both-present idempotent run rc==0")
check(out.getvalue().count("EXISTS") == 2, "both-present run reports both as EXISTS")
check(len(fk.creates) == 0, "both-present run creates nothing new")
# -----------------------------------------------------------------------------
# 4. Partial-present: dc0 already there, dc1 absent -> creates ONLY dc1.
# -----------------------------------------------------------------------------
fk = full_fake(prefixes_extra=[{"prefix": "172.30.2.0/24", "id": 30}])
with captured_stdout():
rc = run_main(["--commit"], fk)
check(rc == 0, "partial-present run rc==0")
check(len(fk.creates) == 1 and fk.creates[0][1]["prefix"] == "172.30.3.0/24",
"partial-present run creates ONLY the missing 172.30.3.0/24",
str([pl["prefix"] for _, pl in fk.creates]))
# -----------------------------------------------------------------------------
# 5. HALF-WRITE GUARD -- a LATER target's missing site aborts with ZERO creates.
# role + container + vr1-dc0 present, but vr1-dc1 SITE absent.
# -----------------------------------------------------------------------------
fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[CONTAINER], sites=[SITE_DC0])
run_dies("half-write: missing vr1-dc1 site is REJECTED before any write", ["--commit"], fk)
check(len(fk.creates) == 0, "half-write: the rejected run wrote NOTHING (vr1-dc0 not created)")
# -----------------------------------------------------------------------------
# 6-8. Missing infra = die (each precondition), each writing nothing.
# -----------------------------------------------------------------------------
fk = fake_netbox.FakeNB(roles=[], prefixes=[CONTAINER], sites=[SITE_DC0, SITE_DC1])
run_dies("missing edge role is REJECTED", ["--commit"], fk)
check(len(fk.creates) == 0, "missing-role run wrote nothing")
fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[], sites=[SITE_DC0, SITE_DC1])
run_dies("missing 172.30.0.0/16 container is REJECTED", ["--commit"], fk)
check(len(fk.creates) == 0, "missing-container run wrote nothing")
fk = fake_netbox.FakeNB(roles=[ROLE], prefixes=[CONTAINER], sites=[SITE_DC1])
run_dies("missing vr1-dc0 site is REJECTED", ["--commit"], fk)
check(len(fk.creates) == 0, "missing-dc0-site run wrote nothing")
# -----------------------------------------------------------------------------
# 9. /24 OUTSIDE the container is rejected by the local subnet preflight.
# -----------------------------------------------------------------------------
_saved = T.TARGETS
T.TARGETS = [("10.0.0.0/24", "vr1-dc0", "bogus -- not inside 172.30.0.0/16")]
try:
run_dies("a /24 outside the Edge container is REJECTED", ["--commit"], full_fake())
finally:
T.TARGETS = _saved
# -----------------------------------------------------------------------------
# 10. Missing NETBOX_URL/TOKEN fails loud (does not guess a target).
# -----------------------------------------------------------------------------
_u = os.environ.pop("NETBOX_URL", None)
run_dies("missing NETBOX_URL fails loud", [], full_fake())
os.environ["NETBOX_URL"] = _u
# -----------------------------------------------------------------------------
# 11. UPSTREAM-WRITE guard: --commit to a non-sandbox host refuses without the flag,
# proceeds with it. Sandbox needs no flag (covered above).
# -----------------------------------------------------------------------------
os.environ["NETBOX_URL"] = "https://netbox.baldurkeep.com" # the production apex
fk = full_fake()
run_dies("upstream --commit WITHOUT --yes-write-upstream is REFUSED", ["--commit"], fk)
check(len(fk.creates) == 0, "the refused upstream --commit wrote NOTHING")
fk = full_fake()
with captured_stdout():
rc = run_main(["--commit", "--yes-write-upstream"], fk)
check(rc == 0 and len(fk.creates) == 2,
"upstream --commit --yes-write-upstream IS allowed and writes both /24s",
str((rc, len(fk.creates))))
os.environ["NETBOX_URL"] = "http://10.10.1.10:8000"
# -----------------------------------------------------------------------------
# 12. Structural pins (a changed constant a behavioral test alone would miss).
# -----------------------------------------------------------------------------
check(T.ROLE_SLUG == "edge", "ROLE_SLUG is 'edge'")
check(T.CONTAINER == "172.30.0.0/16", "CONTAINER is 172.30.0.0/16")
check(T.STATUS == "active", "STATUS is active (like office1-wan .1/24)")
check([c for c, _, _ in T.TARGETS] == ["172.30.2.0/24", "172.30.3.0/24"],
"TARGETS pins exactly the two DC /24s .2 and .3")
check([s for _, s, _ in T.TARGETS] == ["vr1-dc0", "vr1-dc1"],
"TARGETS scopes .2->vr1-dc0 and .3->vr1-dc1 (no swap in the source)")
check(T.SANDBOX_HOSTS == {"localhost", "127.0.0.1", "10.10.1.10"},
"SANDBOX_HOSTS matches the sibling importers")
check("get_nb" in dir(T) and callable(T.get_nb), "get_nb() injection seam exists")
_src = inspect.getsource(T)
check('User-Agent"' in _src and "curl/8.5.0" in _src, "UA-aware (WAF-safe User-Agent set)")
print()
if F == 0:
print(f"test_logic: ALL PASS ({P} checks)")
sys.exit(0)
print(f"test_logic: {F} FAIL of {P + F}")
sys.exit(1)