feat: network topology diagram with Packet-Tracer-style symbols

- New section "Netzwerk-Topologie": one diagram per site built from
  NetBox cable paths (traced through patch panels), layered top-down by
  cabling (provider > firewall/router > core > access > endpoints),
  barycenter ordering, "2x" for parallel links, provider circuits as
  clouds and devices outside the scope dashed. Labels: management IP,
  model, ports.
- New export format "draw.io": the topology as an editable .drawio file
  (one page per site, symbols embedded).
- 14 built-in symbols drawn with Pillow; automatic mapping from role,
  model and manufacturer, passive gear (patch panels, PDUs, UPS) hidden.
- New TopologySymbol model (migration 0002) with list/edit/delete UI,
  symbol gallery, custom icon upload and a "Topologie-Symbol" button on
  device type and device role pages.

Bump version to 0.4.0 - requires "manage.py migrate".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 14:12:55 +02:00
co-authored by Claude Opus 5.5
parent 219a429f3f
commit e477aa9b49
22 changed files with 1147 additions and 17 deletions
+42
View File
@@ -74,3 +74,45 @@ class BrandingProfile(models.Model):
"docx_table_style": self.docx_table_style,
"extra_css": self.extra_css,
}
class TopologySymbol(models.Model):
"""Topologie-Symbol für einen Gerätetyp oder eine Geräterolle (Gerätetyp hat Vorrang)."""
device_type = models.OneToOneField(
"dcim.DeviceType", on_delete=models.CASCADE, null=True, blank=True, related_name="+", verbose_name="Gerätetyp"
)
role = models.OneToOneField(
"dcim.DeviceRole", on_delete=models.CASCADE, null=True, blank=True, related_name="+", verbose_name="Geräterolle"
)
kind = models.CharField(max_length=20, blank=True, verbose_name="Symbol")
icon = models.ImageField(upload_to="customer-export/symbols/", blank=True, verbose_name="Eigenes Icon")
last_updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("device_type__model", "role__name")
verbose_name = "Topologie-Symbol"
verbose_name_plural = "Topologie-Symbole"
def __str__(self):
return str(self.device_type or self.role)
def get_absolute_url(self):
return reverse("plugins:netbox_customer_export:symbol_edit", kwargs={"pk": self.pk})
@property
def target_label(self):
return "Gerätetyp" if self.device_type_id else "Geräterolle"
def get_kind_display(self):
from .symbols import KIND_CHOICES
return dict(KIND_CHOICES).get(self.kind, "Automatisch")
def clean(self):
from django.core.exceptions import ValidationError
if bool(self.device_type_id) == bool(self.role_id):
raise ValidationError("Bitte genau einen Gerätetyp oder eine Geräterolle wählen.")
if not self.kind and not self.icon:
raise ValidationError("Bitte ein Symbol wählen oder ein eigenes Icon hochladen.")