""" Run INSIDE the NetBox Django shell to export the field-switch topology (switches + every patch panel / splice module they physically pass through) as JSON, for generate_drawio.py to render. Usage (from the NetBox host, e.g. /opt/netbox): docker exec netbox-netbox-1 python3 /opt/netbox/netbox/manage.py shell \ -c "$(cat topology/export_topology.py)" 2>/dev/null > topology/topo_full.json The script only reads data -- it never modifies NetBox. If a switch's connected_endpoints comes back empty when it clearly shouldn't, that is a NetBox cable-path bug, not a bug in this script -- see the "gotchas" section of the repo README before assuming the physical link doesn't exist. Two passes: 1. Walk every switch's cable path (Interface.trace()) to find every device -- patch panel, splice module, other switch -- it physically touches. This is just to build the device set; NetBox's cached path is what can go stale (see README), which is why pass 2 doesn't rely on it for the edges themselves. 2. Pull every real Cable among those devices directly, so each edge carries the exact port name on both ends (what generate_drawio.py labels connectors with) instead of just "these two are connected." """ from dcim.models import Device, Interface, Cable import json switch_ids = set( Device.objects.filter( role__slug__in=["core-switch", "dist-switch", "field-switch"] ).values_list("id", flat=True) ) ifaces = Interface.objects.filter( device_id__in=switch_ids, cable__isnull=False ).select_related("device") touched_devices = set(switch_ids) for i in ifaces: path = i._path if not path or not path.is_active: continue for hop in i.trace(): for item in hop: for t in item: dev = getattr(t, "device", None) if dev: touched_devices.add(dev.id) cables = Cable.objects.filter( terminations__termination_type__model__in=["interface", "frontport", "rearport"], ).distinct() edges = {} for c in cables: terms = list(c.terminations.all()) a_side = [t for t in terms if t.cable_end == "A"] b_side = [t for t in terms if t.cable_end == "B"] for ta in a_side: for tb in b_side: da = getattr(ta.termination, "device", None) db = getattr(tb.termination, "device", None) if not da or not db: continue if da.id in touched_devices and db.id in touched_devices and da.id != db.id: key = tuple(sorted((da.id, db.id))) edges[key] = { "a": da.id, "a_port": ta.termination.name, "b": db.id, "b_port": tb.termination.name, } devs = Device.objects.filter(id__in=touched_devices).select_related( "site", "role", "device_type" ) out = { "devices": [ { "id": d.id, "name": d.name, "role": d.role.slug, "role_name": d.role.name, "site": d.site.name if d.site else "", "status": str(d.status), "device_type": d.device_type.model, } for d in devs ], "edges": list(edges.values()), } print(json.dumps(out))