Files
Netbox/topology/export_topology.py
T
dcstephenson b6151b7de5 Add port labels, modernize patch panels, add a real PNG preview
- export_topology.py: edges now carry the exact port name on both ends
  (pulled directly from Cable/CableTermination, not inferred from the
  trace chain), so the diagram can label each connector.
- generate_drawio.py: patch panels are now a small rack-unit shape
  (header strip + port ticks, parented to the panel body so they move
  together) instead of a plain box; connectors show 'porta <-> portb'
  and no longer carry a directional arrowhead (these are physical
  cables, not directed links); switches keep the network-switch icon.
- render_png.js + render_host.html: headless Puppeteer driver for
  embed.diagrams.net's postMessage export protocol -- an actual
  render of the .drawio file, not an approximation. Field-Switch-
  Topology.png is the committed output, for anyone who wants a look
  without opening draw.io.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NM3nivbufxEWiKKmatEtEe
2026-09-15 13:03:18 -05:00

93 lines
3.2 KiB
Python

"""
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))