#!/usr/bin/env python3
"""
Prove a seeded sandbox is a FAITHFUL replica of the upstream draft, plus an
expected delta. Read-only; compares two JSON dumps, touches no NetBox.
WHY THIS EXISTS. Counts and idempotency CANNOT prove fidelity. On 2026-07-13 the
seeder silently dropped the scope on 17 of 90 prefixes (it mapped only
`dcim.site`, and most of the VR1 draft is `dcim.region`-scoped) -- and every
count still matched, and re-running was still perfectly idempotent. It was caught
by luck on a spot-check that happened to print `site None`. This script is the
check that would have caught it on purpose.
It diffs FIELD BY FIELD on every shared object, so a dropped or mangled field is
a hard failure rather than an invisible one.
Usage:
# dump upstream (on vcloud), and the sandbox (on office1-netbox) -- SAME
# read-only dumper, pointed at each in turn:
python3 netbox/prod-draft-dump.py --out netbox/draft/vr1-draft.json
python3 netbox/prod-draft-dump.py --out /tmp/sandbox-dump.json # NETBOX_URL=sandbox
python3 netbox/sandbox-fidelity-check.py \
--upstream netbox/draft/vr1-draft.json --sandbox /tmp/sandbox-dump.json
Exit 0 = faithful replica + exactly the expected delta. Exit 1 = the seeder is
dropping or mangling data.
"""
import argparse
import json
import sys
ap = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
ap.add_argument("--upstream", required=True, help="JSON dump of the upstream draft")
ap.add_argument("--sandbox", required=True, help="JSON dump of the seeded sandbox")
args = ap.parse_args()
up = json.load(open(args.upstream))
sb = json.load(open(args.sandbox))
KEY={"ipam/rirs":"slug","ipam/roles":"slug","dcim/regions":"slug","dcim/sites":"slug",
"ipam/aggregates":"prefix","ipam/prefixes":"prefix",
# D-120: the compose-/24 band ranges + the static service IPs. Ranges key on the
# dumper's synthetic "range" (start-end); IPs on "address".
"ipam/ip-ranges":"range","ipam/ip-addresses":"address"}
# The FULL planned delta the sandbox is allowed to have over the upstream draft.
# Anything outside this is a tooling bug (a silent drop, or a stray write).
# D-115 the office carve (site vr1-off1, edge role, 8 prefixes)
# D-117 VR1 DC0/DC1 six-plane (36 prefixes -- 18 per DC)
# D-118 the org ULA /48 (RIRs + aggregates)
# G1/G2 six-plane roles + aggregates
# DC delta prefixes AND their INTENDED scope (the DC's own site). The DC->site
# binding is the D-119 identity: GUA f02 / ULA nibble 2 / the first six /22s ->
# vr1-dc0; f03 / nibble 3 / the last six -> vr1-dc1. Built in ONE place so the CIDR
# set and its scope map cannot drift apart.
def _dc_prefix_scopes():
scope = {}
v4 = {"vr1-dc0": ["10.12.4.0/22","10.12.8.0/22","10.12.12.0/22",
"10.12.16.0/22","10.12.32.0/22","10.12.36.0/22"], # VR1 DC0 (inherited)
"vr1-dc1": ["10.12.64.0/22","10.12.68.0/22","10.12.72.0/22",
"10.12.76.0/22","10.12.80.0/22","10.12.84.0/22"]} # VR1 DC1 (D-115 /19)
for site, cidrs in v4.items():
for c in cidrs:
scope[c] = ("site", site)
for dc, site in (("f02", "vr1-dc0"), ("f03", "vr1-dc1")):
for c in (f"2602:f3e2:{dc}:10::/60", f"2602:f3e2:{dc}:10::/64", f"2602:f3e2:{dc}:11::/64"):
scope[c] = ("site", site)
for n, site in (("2", "vr1-dc0"), ("3", "vr1-dc1")): # DC nibble
for nn in ("20", "30", "40", "50"):
scope[f"fd50:840e:74e2:{n}{nn}::/60"] = ("site", site)
scope[f"fd50:840e:74e2:{n}{nn}::/64"] = ("site", site)
scope[f"fd50:840e:74e2:{n}21::/64"] = ("site", site) # metal-internal shares metal /60
return scope
def _dc_prefixes():
return set(_dc_prefix_scopes())
# INTENDED scope per DELTA prefix: ("site", slug) / ("region", slug) / None for an
# intentionally UNSCOPED org-level role container. The two role /16s (office, edge)
# are unscoped BY DESIGN in d115-office-carve.py -- mirroring the 27 unscoped role
# containers ALREADY in the upstream apex (10.0.0.0/8, 172.16.0.0/12, ...). Checking
# scope == INTENDED (not merely "has a scope") kills the false "NO SCOPE" failure on
# those two AND still catches a DROPPED scope (the 17-prefix region loss) or a
# WRONG-TARGET binding (a DC0 prefix bound to vr1-dc1 -- the D-117 near-miss).
EXPECTED_PREFIX_SCOPE = {
"10.10.0.0/16": None, # office role container -- unscoped by design
"172.30.0.0/16": None, # edge role container -- unscoped by design
"10.10.0.0/22": ("site", "vr1-off1"),
"10.10.0.0/24": ("site", "vr1-off1"),
"10.10.1.0/24": ("site", "vr1-off1"),
"172.30.1.0/24": ("site", "vr1-off1"),
"2602:f3e2:f01:100::/56": ("site", "vr1-off1"),
"2602:f3e2:f01:100::/64": ("site", "vr1-off1"),
**_dc_prefix_scopes(),
}
EXPECTED_NEW = {
"ipam/roles": {"edge", "provider-public", "metal-admin", "metal-internal",
"data-tenant", "replication"},
"dcim/sites": {"vr1-off1"},
"ipam/rirs": {"rfc-4193-ula", "rfc-1918"},
"ipam/aggregates": {"2602:f3e2::/36", "23.157.124.0/24", "10.0.0.0/8",
"172.16.0.0/12", "fd50:840e:74e2::/48"},
"ipam/prefixes": set(EXPECTED_PREFIX_SCOPE),
# D-120 (must match netbox/d120-compose-bands.py). Ranges keyed start-end (the
# dumper's synthetic "range"); IPs by address. Both carry the /24 mask. These are
# pure delta -- the frozen v1 draft carries no ip-ranges/ip-addresses -- so
# both-bounds already gives endpoint/address correctness (a wrong endpoint is a
# different key: it shows as UNEXPECTED-EXTRA + EXPECTED-BUT-ABSENT).
"ipam/ip-ranges": {"10.10.1.2/24-10.10.1.49/24",
"10.10.1.100/24-10.10.1.200/24",
"10.10.1.201/24-10.10.1.254/24"},
"ipam/ip-addresses": {"10.10.1.10/24", "10.10.1.11/24"},
}
bad=0
for ep,k in KEY.items():
U={r[k]:r for r in up.get(ep,[])}
S={r[k]:r for r in sb.get(ep,[])}
missing=set(U)-set(S)
extra=set(S)-set(U)
exp=EXPECTED_NEW.get(ep,set())
print(f"\n{ep}: upstream={len(U)} sandbox={len(S)}")
if missing:
bad+=1; print(f" !! MISSING FROM SANDBOX ({len(missing)}): {sorted(missing)[:6]}")
unexpected=extra-exp
if unexpected:
bad+=1; print(f" !! UNEXPECTED EXTRA ({len(unexpected)}): {sorted(unexpected)[:6]}")
elif extra:
print(f" + expected D-115 additions ({len(extra)}): {sorted(extra)}")
# 2026-07-14 HARDENING -- THE FALSE-GREEN THIS SCRIPT USED TO RETURN.
# `unexpected = extra - exp` only proves the delta is a SUBSET of what was
# planned. It NEVER asserted the plan was actually CARRIED OUT. If the importer
# created ZERO of the 36 DC prefixes, extra={} -> unexpected={} -> this script
# printed "Nothing lost, nothing stray" and exited 0. EXPECTED_NEW was an upper
# bound masquerading as an assertion. It must be BOTH bounds.
missing_expected = exp - extra
if missing_expected:
bad+=1
print(f" !! EXPECTED-BUT-ABSENT ({len(missing_expected)}): {sorted(missing_expected)[:8]}")
print( " The planned delta was NOT fully applied. A subset-only check would")
print( " have called this run clean -- that is the false green this catches.")
# DELTA SCOPE-CORRECTNESS. The shared-object drift loop below cannot see these
# (they exist only in the sandbox). Assert each delta prefix's scope == its
# INTENDED scope (EXPECTED_PREFIX_SCOPE), target and all -- reading the dumper's
# slug-rewritten scope_site/scope_region (scope_id is popped on the way in). This
# catches a DROPPED scope (the 17-prefix region loss), a WRONG-TARGET binding (a
# DC0 prefix bound to vr1-dc1 -- the D-117 near-miss), and a spurious scope on an
# intentionally-unscoped container -- WITHOUT false-flagging the role /16s that
# are unscoped by design (which the old "has a scope?" heuristic wrongly failed).
if ep == "ipam/prefixes":
for key in sorted(extra & exp):
rec = S[key]
if rec.get("scope_site"):
actual = ("site", rec["scope_site"])
elif rec.get("scope_region"):
actual = ("region", rec["scope_region"])
else:
actual = None
if key not in EXPECTED_PREFIX_SCOPE:
bad+=1; print(f" !! DELTA prefix {key}: no intended scope encoded (extend EXPECTED_PREFIX_SCOPE)")
elif actual != EXPECTED_PREFIX_SCOPE[key]:
bad+=1; print(f" !! DELTA prefix {key}: scope {actual} != intended {EXPECTED_PREFIX_SCOPE[key]}")
if not rec.get("role"):
bad+=1; print(f" !! DELTA prefix {key} has NO ROLE")
# FIELD-LEVEL fidelity on the shared keys -- this is the check that counts cannot make
drift=[]
for key in sorted(set(U)&set(S)):
u,s=U[key],S[key]
for f in set(u)|set(s):
if f.startswith("_"): continue
if u.get(f)!=s.get(f):
drift.append((key,f,u.get(f),s.get(f)))
if drift:
bad+=1
print(f" !! FIELD DRIFT on {len(set(d[0] for d in drift))} object(s), {len(drift)} field(s):")
for key,f,uv,sv in drift[:12]:
print(f" {key:<26} {f:<14} upstream={uv!r} sandbox={sv!r}")
else:
print(" field-level: IDENTICAL on every shared object")
print("\n" + "=" * 70)
if bad == 0:
print("SANDBOX = upstream draft + EXACTLY the planned delta (D-115/D-117/D-118/D-120). Nothing lost, nothing stray.")
sys.exit(0)
print(f"!! {bad} fidelity problem(s) -- the seeder is dropping or mangling data")
sys.exit(1)