diff --git a/netbox/draft/vr1-draft.json b/netbox/draft/vr1-draft.json new file mode 100644 index 0000000..e358f73 --- /dev/null +++ b/netbox/draft/vr1-draft.json @@ -0,0 +1,1132 @@ +{ + "_note": "READ-ONLY dump. IDs discarded; refs are by slug.", + "_source": "https://netbox.baldurkeep.com", + "dcim/regions": [ + { + "description": "Top-level region for US West metros. Holds Eugene (and future West Coast metros).", + "name": "US West", + "parent": null, + "slug": "us-west" + }, + { + "description": "Region0 \u2014 Eugene, OR metro. Nested under US West. Assigned 10.16.0.0/16 IPv4.", + "name": "Eugene", + "parent": "us-west", + "slug": "eugene" + }, + { + "description": "Virtual Regions for testing", + "name": "Virtual Regions", + "parent": null, + "slug": "virtual-region" + }, + { + "description": "Virtual Region for testing", + "name": "VR0", + "parent": "virtual-region", + "slug": "vr0" + }, + { + "description": "Virtual Region for testing", + "name": "VR1", + "parent": "virtual-region", + "slug": "vr1" + } + ], + "dcim/sites": [ + { + "description": "Primary office \u2014 Office0 at 10.16.4.0/24", + "name": "Charnelton", + "region": "eugene", + "slug": "charnelton", + "status": "active" + }, + { + "description": "Future manufacturing/OT \u2014 Cloud3 at 10.16.64.0/20 (reserved)", + "name": "Cleveland", + "region": "eugene", + "slug": "cleveland", + "status": "planned" + }, + { + "description": "Primary DC and current Internet edge \u2014 Cloud0 at 10.16.16.0/20", + "name": "Roosevelt", + "region": "eugene", + "slug": "roosevelt", + "status": "active" + }, + { + "description": "Future tertiary DC \u2014 Cloud2 at 10.16.48.0/20 (reserved)", + "name": "Stevenson", + "region": "eugene", + "slug": "stevenson", + "status": "planned" + }, + { + "description": "Virtual Site for testing", + "name": "VR0 DC0", + "region": "vr0", + "slug": "vr0-dc0", + "status": "active" + }, + { + "description": "Virtual Site for testing", + "name": "VR0 DC1", + "region": "vr0", + "slug": "vr0-dc1", + "status": "active" + }, + { + "description": "Virtual Site for testing", + "name": "VR0 Off0", + "region": "vr0", + "slug": "vr0-off0", + "status": "active" + }, + { + "description": "Virtual Site for testing", + "name": "VR1 DC0", + "region": "vr1", + "slug": "vr1-dc0", + "status": "active" + }, + { + "description": "Virtual Site for testing", + "name": "VR1 DC1", + "region": "vr1", + "slug": "vr1-dc1", + "status": "active" + }, + { + "description": "Secondary DC \u2014 Cloud1 at 10.16.32.0/20. Future Internet edge once ARIN allocation and transit contracts move.", + "name": "Willamette", + "region": "eugene", + "slug": "willamette", + "status": "active" + } + ], + "ipam/aggregates": [], + "ipam/prefixes": [ + { + "description": "Local Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "0.0.0.0/8", + "role": "localnet", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Corp Private", + "is_pool": false, + "mark_utilized": false, + "prefix": "10.0.0.0/8", + "role": "private", + "scope_type": null, + "status": "container" + }, + { + "description": "Virtual Cloud IPs", + "is_pool": false, + "mark_utilized": false, + "prefix": "10.12.0.0/16", + "role": "cloud", + "scope_type": null, + "status": "active" + }, + { + "description": "Current assignment to Willamette dev cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "10.16.0.0/16", + "role": "dc", + "scope_type": null, + "status": "deprecated" + }, + { + "description": "Current assignment to Roosevelt dev cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "10.17.0.0/16", + "role": "dc", + "scope_type": null, + "status": "deprecated" + }, + { + "description": "Public ARIN assignment to Abysius Inc", + "is_pool": false, + "mark_utilized": false, + "prefix": "23.157.124.0/24", + "role": "public", + "scope_type": null, + "status": "container" + }, + { + "description": "CGNAT Reserved", + "is_pool": false, + "mark_utilized": false, + "prefix": "100.64.0.0/10", + "role": "cgnat", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Localhost", + "is_pool": false, + "mark_utilized": false, + "prefix": "127.0.0.0/8", + "role": "localhost", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Link-Local", + "is_pool": false, + "mark_utilized": false, + "prefix": "169.254.0.0/16", + "role": "link-local", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Corp OOB Private", + "is_pool": false, + "mark_utilized": false, + "prefix": "172.16.0.0/12", + "role": "private", + "scope_type": null, + "status": "container" + }, + { + "description": "Residential Private", + "is_pool": false, + "mark_utilized": false, + "prefix": "192.168.0.0/16", + "role": "private", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Public Multicast", + "is_pool": false, + "mark_utilized": false, + "prefix": "224.0.0.0/4", + "role": "multicast", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Public Reserved - Enterprise Private", + "is_pool": false, + "mark_utilized": false, + "prefix": "240.0.0.0/4", + "role": "private", + "scope_type": null, + "status": "container" + }, + { + "description": "Localhost", + "is_pool": false, + "mark_utilized": false, + "prefix": "::1/128", + "role": "localhost", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Discard Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "100::/64", + "role": "discard", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Public ARIN assignment to Abysius Inc", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2::/36", + "role": "public", + "scope_type": null, + "status": "container" + }, + { + "description": "Inter-Regional Infrastructure", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2::/40", + "role": "infra", + "scope_type": null, + "status": "container" + }, + { + "description": "Inter-Regional Site to Site Links", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2::/48", + "role": "infra", + "scope_type": null, + "status": "container" + }, + { + "description": "Edge Networks", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:fe::/48", + "role": null, + "scope_type": null, + "status": "container" + }, + { + "description": "Private cloud tenant reservation", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:ff::/48", + "role": "openstack-tenant", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Eugene Region; Sites receive /24 allocations starting at fc01:0100::/24", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:100::/40", + "role": "region", + "scope_region": "eugene", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Eugene Regional Infrastructure", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:100::/48", + "role": "infra", + "scope_region": "eugene", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Eugene Site to Site Links", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:100::/56", + "role": "infra", + "scope_region": "eugene", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Eugene Regional Offices; Offices are allocated /32 subnets", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:101::/48", + "role": "office", + "scope_region": "eugene", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Reserved for fun and profit", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:101::/56", + "role": "office", + "scope_region": "eugene", + "scope_type": "dcim.region", + "status": "reserved" + }, + { + "description": "Charnelton", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:101:100::/56", + "role": "office", + "scope_site": "charnelton", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Willamette Site", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102::/48", + "role": "dc", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Omega Cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102::/56", + "role": "cloud", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:10::/60", + "role": "provider", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:10::/64", + "role": "provider", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "Willamette Omega Cloud \u2014 Provider API VIPs (charm-managed via hacluster; excluded from Neutron pools)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:11::/64", + "role": "provider", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:20::/60", + "role": "metal", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:20::/64", + "role": "metal", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:30::/60", + "role": "data", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:30::/64", + "role": "data", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:40::/60", + "role": "storage", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:40::/64", + "role": "storage", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:50::/60", + "role": "repl", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:50::/64", + "role": "repl", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:80::/60", + "role": "lbaas-mgmt", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:80::/64", + "role": "lbaas-mgmt", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:e0::/60", + "role": "vpn", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:f0::/60", + "role": "oob", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:f0::/64", + "role": "oob", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "Psi Cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:100::/56", + "role": "cloud", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Beta Cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:1600::/56", + "role": "cloud", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Alpha Cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:102:1700::/56", + "role": "cloud", + "scope_site": "willamette", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Roosevelt Site", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:103::/48", + "role": "dc", + "scope_site": "roosevelt", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Second Region", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:200::/40", + "role": "region", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Third Region", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:300::/40", + "role": "region", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Virtual Region 0 (VR0)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e00::/40", + "role": "region", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Virtual Region 0 (VR0) Infrastructure", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e00::/48", + "role": "infra", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "VR0 Site to Site Links", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e00::/56", + "role": "infra", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "active" + }, + { + "description": "VR0 DC1 to DC0 Site to Site Link", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e00:1::/127", + "role": "infra", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "active" + }, + { + "description": "VR0 Off0 to DC0 Site to Site Link", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e00:2::/127", + "role": "infra", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "active" + }, + { + "description": "Virtual Region 0 (VR0) Offices", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e01::/48", + "role": "office", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Virtual Region 0 (VR0) Offices; Reserved for fun and profit", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e01::/56", + "role": "office", + "scope_region": "vr0", + "scope_type": "dcim.region", + "status": "reserved" + }, + { + "description": "Virtual Region 0 (VR0) Office 0 (Off0)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e01:100::/56", + "role": "office", + "scope_site": "vr0-off0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 Off0 Office Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e01:100::/64", + "role": "office", + "scope_site": "vr0-off0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "Virtual Region 0 (VR0) Datacenter 0 (DC0)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02::/48", + "role": "dc", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02::/56", + "role": "cloud", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud Provider Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:10::/60", + "role": "provider", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud Provider Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:10::/64", + "role": "provider", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud \u2014 Provider API VIPs (charm-managed via hacluster; excluded from Neutron pools)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:11::/64", + "role": "provider", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud Metal Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:20::/60", + "role": "metal", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud Metal Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:20::/64", + "role": "metal", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud Data Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:30::/60", + "role": "data", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud Data Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:30::/64", + "role": "data", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud Storage Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:40::/60", + "role": "storage", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud Storage Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:40::/64", + "role": "storage", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud Replication Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:50::/60", + "role": "repl", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud Replication Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:50::/64", + "role": "repl", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud LBaaS Management Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:80::/60", + "role": "lbaas-mgmt", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud LBaaS Management Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:80::/64", + "role": "lbaas-mgmt", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "VR0 DC0 Omega Cloud VPN Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:e0::/60", + "role": "vpn", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud OOB Prefix", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:f0::/60", + "role": "oob", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "VR0 DC0 Omega Cloud OOB Subnet", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e02:f0::/64", + "role": "oob", + "scope_site": "vr0-dc0", + "scope_type": "dcim.site", + "status": "active" + }, + { + "description": "Virtual Region 0 (VR0) Datacenter 1 (DC1)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:e03::/48", + "role": "dc", + "scope_site": "vr0-dc1", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Virtual Region 1 (VR1)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f00::/40", + "role": "region", + "scope_region": "vr1", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Virtual Region 1 (VR1) Infrastructure", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f00::/48", + "role": "infra", + "scope_region": "vr1", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "VR1 Site to Site Links", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f00::/56", + "role": "infra", + "scope_region": "vr1", + "scope_type": "dcim.region", + "status": "active" + }, + { + "description": "Virtual Region 1 (VR1) Offices", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f01::/48", + "role": "office", + "scope_region": "vr1", + "scope_type": "dcim.region", + "status": "container" + }, + { + "description": "Virtual Region 1 (VR1) Offices; Reserved for fun and profit", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f01::/56", + "role": "office", + "scope_region": "vr1", + "scope_type": "dcim.region", + "status": "reserved" + }, + { + "description": "Virtual Region 1 (VR1) Datacenter 0 (DC0)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f02::/48", + "role": "dc", + "scope_site": "vr1-dc0", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Virtual Region 1 (VR1) Datacenter 1 (DC1)", + "is_pool": false, + "mark_utilized": false, + "prefix": "2602:f3e2:f03::/48", + "role": "dc", + "scope_site": "vr1-dc1", + "scope_type": "dcim.site", + "status": "container" + }, + { + "description": "Centrally assigned ULA (Not Active)", + "is_pool": false, + "mark_utilized": false, + "prefix": "fc00::/8", + "role": "private", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Unique Local Addresses (ULA)", + "is_pool": false, + "mark_utilized": false, + "prefix": "fd00::/8", + "role": "private", + "scope_type": null, + "status": "container" + }, + { + "description": "Tailscale Addresses", + "is_pool": false, + "mark_utilized": true, + "prefix": "fd7a:115c:a1e0::/48", + "role": "vpn", + "scope_type": null, + "status": "active" + }, + { + "description": "Link-Local", + "is_pool": false, + "mark_utilized": false, + "prefix": "fe80::/10", + "role": "link-local", + "scope_type": null, + "status": "reserved" + }, + { + "description": "Public Multicast", + "is_pool": false, + "mark_utilized": false, + "prefix": "ff00::/8", + "role": "multicast", + "scope_type": null, + "status": "reserved" + } + ], + "ipam/rirs": [ + { + "description": "", + "is_private": false, + "name": "ARIN", + "slug": "arin" + } + ], + "ipam/roles": [ + { + "description": "", + "name": "CGNAT", + "slug": "cgnat", + "weight": 1000 + }, + { + "description": "Cloud container", + "name": "Cloud", + "slug": "cloud", + "weight": 1000 + }, + { + "description": "Openstack data plane - shared across datacenters", + "name": "Data", + "slug": "data", + "weight": 1000 + }, + { + "description": "Datacenter container", + "name": "Datacenter", + "slug": "dc", + "weight": 1000 + }, + { + "description": "", + "name": "Discard", + "slug": "discard", + "weight": 1000 + }, + { + "description": "Shared infrastructure \u2014 ptp transit, device loopbacks, RR loopbacks, anycast.", + "name": "Infrastructure", + "slug": "infra", + "weight": 1000 + }, + { + "description": "Load-balancer management (Octavia control plane, not dataplane).", + "name": "LBaaS Management", + "slug": "lbaas-mgmt", + "weight": 1000 + }, + { + "description": "", + "name": "Link-Local", + "slug": "link-local", + "weight": 1000 + }, + { + "description": "", + "name": "Localhost", + "slug": "localhost", + "weight": 1000 + }, + { + "description": "", + "name": "Localnet", + "slug": "localnet", + "weight": 1000 + }, + { + "description": "Bare-metal server NICs + bundled infra: OpenStack control plane, in-band switch mgmt (VLAN 50), Edge-eBGP/DDoS sub-ranges (Roosevelt), RR loopback references. Sized as 2x physical server count.", + "name": "Metal", + "slug": "metal", + "weight": 1000 + }, + { + "description": "", + "name": "Multicast", + "slug": "multicast", + "weight": 1000 + }, + { + "description": "Office site allocation (/24 within Region0 /20 reserved block).", + "name": "Office", + "slug": "office", + "weight": 1000 + }, + { + "description": "Out of band management network", + "name": "OOB", + "slug": "oob", + "weight": 1000 + }, + { + "description": "OpenStack tenant pool \u2014 shared across tenants", + "name": "Openstack Tenant", + "slug": "openstack-tenant", + "weight": 1000 + }, + { + "description": "Reserved addresses for private networks", + "name": "Private", + "slug": "private", + "weight": 1000 + }, + { + "description": "Tenant-reachable. Carries ext_net (FIPs, SNAT egress) and OpenStack public API VIPs. Within each /60: first /64 Neutron FIP pool; next /64 charm API VIPs. Neutron allocation_pools must exclude VIPs.", + "name": "Provider", + "slug": "provider", + "weight": 1000 + }, + { + "description": "Public addresses assigned by a RIR", + "name": "Public", + "slug": "public", + "weight": 1000 + }, + { + "description": "", + "name": "Region", + "slug": "region", + "weight": 1000 + }, + { + "description": "RBD mirror for cross-site Ceph replication. Routed via EVPN type-5 between DCs.", + "name": "Replication", + "slug": "repl", + "weight": 1000 + }, + { + "description": "Ceph-Public (L3-routed) + Ceph-Cluster (non-routed VLAN inside /23 L2).", + "name": "Storage", + "slug": "storage", + "weight": 1000 + }, + { + "description": "OpenStack tenant networks (Geneve underlay touchdown). Also absorbs DMZ roles under regional scheme.", + "name": "Tenant", + "slug": "tenant", + "weight": 1000 + }, + { + "description": "VPN client pools \u2014 site-independent (10.2.0.0/16 Corp Private).", + "name": "VPN", + "slug": "vpn", + "weight": 1000 + } + ] +} diff --git a/netbox/prod-draft-dump.py b/netbox/prod-draft-dump.py new file mode 100755 index 0000000..05267d9 --- /dev/null +++ b/netbox/prod-draft-dump.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Dump the UPSTREAM NetBox draft to a reviewable JSON artifact. READ-ONLY. + +This is phase 1 of the two-phase sandbox seeding loop documented in +docs/session-ledger.md: + + upstream (draft source) --[this script, READ-ONLY]--> vr1-draft.json + vr1-draft.json --[netbox/sandbox-seed.py]--> Office1 sandbox + ...simulate VR1 in the sandbox, apply planned changes (D-115/D-117)... + validated refinements --[operator-gated]----------> upstream + +WHY TWO PHASES instead of one source->dest script: + + 1. The upstream token NEVER travels. This script runs on vcloud (where + ~/.vr1-netbox.env lives); sandbox-seed.py runs on office1-netbox (where + the sandbox token lives). Neither host holds the other's credential. + 2. "The sim NEVER writes upstream" becomes STRUCTURAL, not a discipline: the + script that writes has no upstream credentials and no upstream URL. + 3. The JSON dump is a reviewable, diffable artifact. You can read exactly what + is about to be seeded before anything is written. + +This script performs NO writes of any kind. It has no --commit flag because it +has nothing to commit. + +TRAP -- User-Agent (measured 2026-07-13, see references/platform-traps.md): +upstream NetBox sits behind a filter that 403s the default Python User-Agent. +The SAME token works from curl. It looks exactly like an auth failure and it is +not. We send an accepted UA. pynetbox (python-requests/...) is bitten too. + +Usage: + . ~/.vr1-netbox.env # NETBOX_URL + NETBOX_TOKEN + python3 netbox/prod-draft-dump.py --out netbox/draft/vr1-draft.json +""" + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request + +# Upstream is 4.5.8 and uses v1 (bare, 40-char) tokens. The Office1 sandbox is +# 4.6.4 and uses v2 (nbt_.). Do NOT assume one shape fits both. +UA = "curl/8.5.0" + +# What the VR1 carve actually needs to exist. Order is DEPENDENCY order and the +# seeder replays it verbatim -- parents before children. +ENDPOINTS = [ + ("ipam/rirs", ["name", "slug", "is_private", "description"]), + ("ipam/roles", ["name", "slug", "weight", "description"]), + ("dcim/regions", ["name", "slug", "parent", "description"]), + ("dcim/sites", ["name", "slug", "status", "region", "description"]), + ("ipam/aggregates", ["prefix", "rir", "description"]), + ("ipam/prefixes", ["prefix", "status", "role", "scope_type", "scope_id", + "description", "is_pool", "mark_utilized"]), +] + + +def die(msg: str) -> "None": + print(f"FAIL: {msg}", file=sys.stderr) + sys.exit(2) + + +def require_env(name: str) -> str: + v = os.environ.get(name) + if not v: + die(f"{name} is not set. Source the upstream env first: . ~/.vr1-netbox.env") + return v + + +def get_all(base: str, token: str, path: str) -> list: + """Paginate an endpoint fully. Read-only.""" + out = [] + url = f"{base}/api/{path}/?limit=200" + while url: + req = urllib.request.Request(url, headers={ + "Authorization": f"Token {token}", + "Accept": "application/json", + "User-Agent": UA, # <-- the WAF trap. Do not remove. + }) + try: + with urllib.request.urlopen(req, timeout=45) as r: + data = json.load(r) + except urllib.error.HTTPError as exc: + if exc.code == 403: + die(f"403 on {path}. If curl works with this same token, it is the " + f"User-Agent filter, NOT the token -- see references/platform-traps.md.") + die(f"HTTP {exc.code} on {path}: {exc.reason}") + out += data["results"] + nxt = data.get("next") + url = nxt if nxt else None + return out + + +def slim(obj: dict, fields: list) -> dict: + """Keep only what the seeder needs, resolving nested refs to SLUGS. + + IDs are deliberately DISCARDED. A sandbox assigns its own ids; carrying + upstream ids across would either collide or silently mis-link. Everything is + re-linked by slug/prefix on the way in. + """ + rec = {} + for f in fields: + v = obj.get(f) + if isinstance(v, dict): + # nested object (role, rir, region, parent, status) -> slug or value + rec[f] = v.get("slug") or v.get("value") + else: + rec[f] = v + return rec + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0]) + ap.add_argument("--out", required=True, help="Path to write the JSON draft artifact") + args = ap.parse_args() + + base = require_env("NETBOX_URL").rstrip("/") + token = require_env("NETBOX_TOKEN") + + # Site id -> slug, so prefix scope_id (an upstream id) can be rewritten to a + # slug the seeder can re-resolve against ITS OWN site ids. + print(f"Reading upstream draft from {base} (READ-ONLY) ...") + raw_sites = get_all(base, token, "dcim/sites") + site_id_to_slug = {s["id"]: s["slug"] for s in raw_sites} + # Prefixes can be scoped to a REGION as well as a site -- and most of the VR1 + # draft is region-scoped (the f00/f01 containers hang off region VR1, not off + # a site). Mapping only dcim.site silently DROPPED those scopes on the way in. + raw_regions = get_all(base, token, "dcim/regions") + region_id_to_slug = {r["id"]: r["slug"] for r in raw_regions} + + draft = {"_source": base, "_note": "READ-ONLY dump. IDs discarded; refs are by slug."} + for path, fields in ENDPOINTS: + rows = raw_sites if path == "dcim/sites" else get_all(base, token, path) + slimmed = [] + for o in rows: + rec = slim(o, fields) + if path == "ipam/prefixes": + # scope_id is an upstream id -- rewrite to a slug, or drop the scope. + sid = o.get("scope_id") + st = o.get("scope_type") + if sid is not None and st == "dcim.site": + rec["scope_site"] = site_id_to_slug.get(sid) + elif sid is not None and st == "dcim.region": + rec["scope_region"] = region_id_to_slug.get(sid) + rec.pop("scope_id", None) + slimmed.append(rec) + draft[path] = slimmed + print(f" {path:<18} {len(slimmed)}") + + os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) + with open(args.out, "w") as fh: + json.dump(draft, fh, indent=2, sort_keys=True) + fh.write("\n") + print(f"\nWrote {args.out}. NOTHING was written to NetBox.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/netbox/sandbox-seed.py b/netbox/sandbox-seed.py new file mode 100755 index 0000000..83e8a0b --- /dev/null +++ b/netbox/sandbox-seed.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +Seed a SANDBOX NetBox from the JSON draft produced by netbox/prod-draft-dump.py. + +Phase 2 of the two-phase seeding loop (see prod-draft-dump.py for the why). + +THIS SCRIPT CANNOT WRITE UPSTREAM. It has no upstream URL and no upstream token +-- it only knows the JSON file and the sandbox it is pointed at. That is +deliberate: "the sim never writes upstream" is enforced by STRUCTURE, not by +remembering to pass the right flag. + +DRY BY DEFAULT. Nothing is written without --commit. (The repo learned this the +hard way: dc-dc-prefixes-import.py used to write with no flag at all, and would +have silently bound VR1 DC0's prefixes to the wrong site -- D-117.) + +Idempotent: existing objects are detected by natural key (slug, or the prefix +itself) and skipped. Re-running is safe. + +IDs are never carried across. The draft refs everything by slug; this script +re-resolves those against the SANDBOX's own ids. + +Auth: the sandbox (NetBox 4.6) uses v2 tokens -- the wire form is +`nbt_<key>.<plaintext>`. The API's `token` field alone is NOT usable (it parses +as a legacy v1 token and 403s with "Invalid v1 token", which is thoroughly +misleading). Pass the ASSEMBLED value. + +Usage (on office1-netbox, where NetBox is on localhost and the token lives): + sudo NETBOX_URL=http://localhost:8000 \ + NETBOX_TOKEN="$(cat /root/netbox-secrets/api.token)" \ + python3 sandbox-seed.py --draft vr1-draft.json # preview + ... --draft vr1-draft.json --commit # apply +""" + +import argparse +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request + +# Seeding order is DEPENDENCY order: a site needs its region; a prefix needs its +# role and its site. Prefixes do NOT need parent-prefix ordering -- NetBox +# derives the hierarchy from the CIDR itself. +ORDER = ["ipam/rirs", "ipam/roles", "dcim/regions", "dcim/sites", + "ipam/aggregates", "ipam/prefixes"] + +# Natural key per endpoint -- how we decide "does this already exist?" +NATURAL_KEY = { + "ipam/rirs": "slug", + "ipam/roles": "slug", + "dcim/regions": "slug", + "dcim/sites": "slug", + "ipam/aggregates": "prefix", + "ipam/prefixes": "prefix", +} + + +def die(msg: str): + print(f"FAIL: {msg}", file=sys.stderr) + sys.exit(2) + + +class NB: + def __init__(self, base: str, token: str): + self.base = base.rstrip("/") + self.token = token + + def _req(self, method: str, path: str, body=None): + url = f"{self.base}/api/{path}" + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method, headers={ + "Authorization": f"Token {self.token}", + "Accept": "application/json", + "Content-Type": "application/json", + }) + try: + with urllib.request.urlopen(req, timeout=45) as r: + return json.load(r) if r.status != 204 else None + except urllib.error.HTTPError as exc: + detail = exc.read().decode(errors="replace")[:300] + if exc.code == 403 and "v1 token" in detail: + die("403 'Invalid v1 token'. This NetBox is 4.6: the token must be " + "the ASSEMBLED v2 form nbt_<key>.<plaintext>, not the API's bare " + "`token` field. See docs/vr1-office1-as-built.md.") + die(f"HTTP {exc.code} {method} {path}: {detail}") + + def get_one(self, path: str, **flt): + q = urllib.parse.urlencode(flt) + res = self._req("GET", f"{path}/?{q}&limit=1") + return res["results"][0] if res["results"] else None + + def create(self, path: str, payload: dict): + return self._req("POST", f"{path}/", payload) + + def patch(self, path: str, payload: dict): + return self._req("PATCH", f"{path}/", payload) + + +def resolve(nb: NB, cache: dict, path: str, slug: str): + """slug -> sandbox id, cached. None if the slug is absent.""" + if slug is None: + return None + key = (path, slug) + if key not in cache: + obj = nb.get_one(path, slug=slug) + cache[key] = obj["id"] if obj else None + return cache[key] + + +def dep(nb: NB, cache: dict, planned: set, path: str, slug: str, commit: bool): + """Resolve a dependency to (ok, id). + + In a DRY RUN the dependency may not exist yet because we are not creating + anything -- but it IS in the plan, and will exist by the time we commit. + Treating that as "missing" made every prefix cascade-skip behind its role, + which reported a plan of 27/129 and looked like the draft was broken. A dry + run must model the END STATE, not the current empty one. + """ + if slug is None: + return True, None + rid = resolve(nb, cache, path, slug) + if rid is not None: + return True, rid + if not commit and (path, slug) in planned: + return True, None # will exist; no id needed, we never POST + return False, None + + +def build_payload(nb: NB, cache: dict, ep: str, rec: dict, planned: set, commit: bool): + """Draft record -> sandbox POST body, re-linking every ref by slug.""" + p = {k: v for k, v in rec.items() + if k not in ("role", "rir", "region", "parent", "scope_site", + "scope_region", "scope_type", "status") and v is not None} + + if rec.get("status"): + p["status"] = rec["status"] + + if ep == "dcim/regions" and rec.get("parent"): + ok, rid = dep(nb, cache, planned, "dcim/regions", rec["parent"], commit) + if not ok: + return None, f"parent region '{rec['parent']}' not present" + p["parent"] = rid + + if ep == "dcim/sites" and rec.get("region"): + ok, rid = dep(nb, cache, planned, "dcim/regions", rec["region"], commit) + if not ok: + return None, f"region '{rec['region']}' not present" + p["region"] = rid + + if ep == "ipam/aggregates": + ok, rid = dep(nb, cache, planned, "ipam/rirs", rec.get("rir"), commit) + if not ok: + return None, f"rir '{rec.get('rir')}' not present" + p["rir"] = rid + + if ep == "ipam/prefixes": + if rec.get("role"): + ok, rid = dep(nb, cache, planned, "ipam/roles", rec["role"], commit) + if not ok: + return None, f"role '{rec['role']}' not present" + p["role"] = rid + if rec.get("scope_site"): + ok, sid = dep(nb, cache, planned, "dcim/sites", rec["scope_site"], commit) + if not ok: + return None, f"site '{rec['scope_site']}' not present" + p["scope_type"] = "dcim.site" + if sid is not None: + p["scope_id"] = sid + elif rec.get("scope_region"): + ok, rid = dep(nb, cache, planned, "dcim/regions", rec["scope_region"], commit) + if not ok: + return None, f"region '{rec['scope_region']}' not present" + p["scope_type"] = "dcim.region" + if rid is not None: + p["scope_id"] = rid + return p, None + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0]) + ap.add_argument("--draft", required=True, help="JSON produced by prod-draft-dump.py") + ap.add_argument("--commit", action="store_true", + help="WRITE to the sandbox. Default is a DRY RUN that writes nothing.") + ap.add_argument("--update", action="store_true", + help="Also PATCH objects that already exist so they match the draft " + "(otherwise existing objects are left alone). Requires --commit.") + args = ap.parse_args() + + url = os.environ.get("NETBOX_URL") + token = os.environ.get("NETBOX_TOKEN") + if not url or not token: + die("NETBOX_URL and NETBOX_TOKEN must be set (the SANDBOX's, not upstream's).") + + with open(args.draft) as fh: + draft = json.load(fh) + + src = draft.get("_source", "?") + + # ----------------------------------------------------------------------- + # SAFETY GUARD -- runs BEFORE anything else touches the network. + # + # Never write to the instance the draft was READ from. This is + # self-configuring: the draft records its own _source, so the guard keeps + # working if upstream ever moves hosts. A hardcoded hostname denylist would + # silently stop protecting the day the URL changed -- which is exactly the + # class of stale-literal bug this repo keeps finding. + # + # An earlier version of this guard sat AFTER json.load() and after the + # banner. It was dead code for any malformed draft, and it read as "safe" + # to a reviewer. Guards go FIRST. + # ----------------------------------------------------------------------- + def host(u: str) -> str: + return (urllib.parse.urlparse(u).hostname or u).lower() + + if src != "?" and host(url) == host(src): + die(f"REFUSING: NETBOX_URL points at the UPSTREAM NetBox ({host(src)}) -- the " + f"instance this draft was READ FROM. This script seeds a SANDBOX only; " + f"the sim never writes upstream.") + + nb = NB(url, token) + print(f"Draft source : {src}") + print(f"Sandbox : {url}") + if not args.commit: + print("\n*** DRY RUN -- nothing will be written. Re-run with --commit to apply. ***") + else: + print("\n*** COMMITTING to the sandbox. ***") + + cache, planned = {}, set() + created = existing = skipped = updated = 0 + + for ep in ORDER: + rows = draft.get(ep, []) + if not rows: + continue + print(f"\n{ep} ({len(rows)} in draft)") + nkey = NATURAL_KEY[ep] + for rec in rows: + nval = rec.get(nkey) + found = nb.get_one(ep, **{nkey: nval}) + if found: + existing += 1 + cache[(ep, nval)] = found["id"] + if args.update: + payload, why = build_payload(nb, cache, ep, rec, planned, args.commit) + if payload is None: + print(f" SKIP {nval} -- {why}") + continue + if not args.commit: + print(f" [dry-run] would UPDATE {nval}") + continue + nb.patch(f"{ep}/{found['id']}", payload) + print(f" UPDATED {nval} (id={found['id']})") + updated += 1 + continue + payload, why = build_payload(nb, cache, ep, rec, planned, args.commit) + if payload is None: + print(f" SKIP {nval} -- {why}") + skipped += 1 + continue + if not args.commit: + print(f" [dry-run] would CREATE {nval}") + planned.add((ep, nval)) + created += 1 + continue + obj = nb.create(ep, payload) + cache[(ep, nval)] = obj["id"] + print(f" CREATED {nval} (id={obj['id']})") + created += 1 + + verb = "would create" if not args.commit else "created" + print(f"\n{'='*66}") + print(f"{verb}: {created} already present: {existing} updated: {updated} skipped: {skipped}") + if not args.commit: + print("DRY RUN -- the sandbox was NOT modified. Re-run with --commit.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/sandbox-seed/run-tests.sh b/tests/sandbox-seed/run-tests.sh new file mode 100755 index 0000000..58beb48 --- /dev/null +++ b/tests/sandbox-seed/run-tests.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# tests/sandbox-seed/run-tests.sh +# +# Harness for netbox/sandbox-seed.py and netbox/prod-draft-dump.py -- the +# two-phase upstream->sandbox seeding loop. +# +# OFFLINE. Touches no NetBox, real or sandbox. It exercises the arg contract and +# the SAFETY PROPERTIES, which are the whole reason these two scripts are split: +# +# 1. prod-draft-dump.py must have NO write path at all (no --commit, no POST). +# 2. sandbox-seed.py must be DRY BY DEFAULT and must REFUSE an upstream URL. +# 3. The upstream User-Agent trap must stay encoded (a 403 that looks like auth). +# +# Each of these cost real time on 2026-07-13. A regression that quietly drops one +# would not surface until something was written to the production IPAM apex. +# ASCII + LF. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NBDIR="$(cd "$HERE/../../netbox" && pwd)" +SEED="$NBDIR/sandbox-seed.py" +DUMP="$NBDIR/prod-draft-dump.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; } + +# 0. both parse +python3 -c "import ast;ast.parse(open('$SEED').read())" 2>/dev/null && ok || bad "sandbox-seed.py does not parse" +python3 -c "import ast;ast.parse(open('$DUMP').read())" 2>/dev/null && ok || bad "prod-draft-dump.py does not parse" + +# 1. arg contract +python3 "$SEED" --help >/dev/null 2>&1 && ok || bad "sandbox-seed --help -> 0" +python3 "$DUMP" --help >/dev/null 2>&1 && ok || bad "prod-draft-dump --help -> 0" +python3 "$SEED" >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "sandbox-seed with no --draft must fail" +python3 "$DUMP" >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "prod-draft-dump with no --out must fail" + +# 2. THE DUMP HAS NO WRITE PATH. It reads production. If it ever grows a POST, +# that is a write to the IPAM apex from a script whose whole contract is +# "read-only", and no reviewer would be looking for it. +grep -qE 'add_argument\("--commit"' "$DUMP" && bad "prod-draft-dump.py has a --commit flag -- it MUST be read-only" || ok +grep -qE '"(POST|PATCH|PUT|DELETE)"' "$DUMP" && bad "prod-draft-dump.py contains a WRITE method -- it MUST be read-only" || ok + +# 3. sandbox-seed is DRY BY DEFAULT (the D-117 lesson: a write-by-default IPAM +# tool is how an off-by-one lands in production unannounced). +grep -q '"--commit", action="store_true"' "$SEED" && ok || bad "sandbox-seed has no --commit flag" +grep -q 'DRY RUN -- nothing will be written' "$SEED" && ok || bad "sandbox-seed does not announce its dry run" + +# 4. sandbox-seed must REFUSE to point at upstream. "The sim never writes +# upstream" is enforced structurally, not by remembering a flag. +grep -q 'REFUSING: NETBOX_URL points at the UPSTREAM' "$SEED" && ok \ + || bad "sandbox-seed lost its upstream-URL refusal guard" +grep -q 'Guards go FIRST' "$SEED" && ok \ + || bad "the upstream guard is no longer documented as running FIRST (it was dead code once)" +tmp="$(mktemp)"; printf '{"_source":"https://netbox.baldurkeep.com"}' > "$tmp" +out="$(NETBOX_URL=https://netbox.baldurkeep.com NETBOX_TOKEN=x python3 "$SEED" --draft "$tmp" --commit 2>&1)" +rm -f "$tmp" +printf '%s' "$out" | grep -q "REFUSING" && ok \ + || bad "sandbox-seed did NOT refuse an upstream NETBOX_URL (it would have written to production)" + +# 5. the upstream User-Agent trap: a 403 that is NOT an auth failure. +grep -q 'User-Agent' "$DUMP" && ok || bad "prod-draft-dump lost the User-Agent header -- upstream 403s the default python UA" +grep -q 'User-Agent filter, NOT the token' "$DUMP" && ok \ + || bad "prod-draft-dump lost the 403 explanation -- the next person will blame the token" + +# 6. ids must never be carried across instances. +grep -q 'IDs are deliberately DISCARDED' "$DUMP" && ok || bad "prod-draft-dump lost the id-discard contract" + +# 7. region scopes. Mapping only dcim.site silently dropped the scope on 17 of the +# 90 upstream prefixes -- including the ENTIRE VR1 region-scoped draft. Caught +# only because a spot-check printed 'site None'. +grep -q 'dcim.region' "$DUMP" && ok || bad "prod-draft-dump no longer carries REGION-scoped prefixes (17 of 90 upstream)" +grep -q 'dcim.region' "$SEED" && ok || bad "sandbox-seed no longer resolves REGION scopes" + +echo +total=$((pass+fail)) +if [ "$fail" -eq 0 ]; then echo "sandbox-seed: $pass/$total PASS"; exit 0; fi +echo "sandbox-seed: $fail/$total FAIL"; exit 1