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
This commit is contained in:
+34
-10
@@ -12,8 +12,18 @@ 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
|
||||
from dcim.models import Device, Interface, Cable
|
||||
import json
|
||||
|
||||
switch_ids = set(
|
||||
@@ -27,24 +37,38 @@ ifaces = Interface.objects.filter(
|
||||
).select_related("device")
|
||||
|
||||
touched_devices = set(switch_ids)
|
||||
edges = set() # (device_id_a, device_id_b), deduplicated, any two adjacent devices on a physical chain
|
||||
|
||||
for i in ifaces:
|
||||
path = i._path
|
||||
if not path or not path.is_active:
|
||||
continue
|
||||
chain = []
|
||||
for hop in i.trace():
|
||||
for item in hop:
|
||||
for t in item:
|
||||
dev = getattr(t, "device", None)
|
||||
if dev:
|
||||
if not chain or chain[-1] != dev.id:
|
||||
chain.append(dev.id)
|
||||
touched_devices.add(dev.id)
|
||||
for a, b in zip(chain, chain[1:]):
|
||||
if a != b:
|
||||
edges.add(tuple(sorted((a, b))))
|
||||
|
||||
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"
|
||||
@@ -63,6 +87,6 @@ out = {
|
||||
}
|
||||
for d in devs
|
||||
],
|
||||
"edges": [[a, b] for a, b in sorted(edges)],
|
||||
"edges": list(edges.values()),
|
||||
}
|
||||
print(json.dumps(out))
|
||||
|
||||
Reference in New Issue
Block a user