"""Rack-Ansichten offline (ohne HTTP-Aufruf der API) als SVG erzeugen."""
import base64
import logging
import mimetypes
import re
from django.utils.module_loading import import_string
from netbox.plugins import get_plugin_config
logger = logging.getLogger("netbox_customer_export")
# NetBox-Standardwerte der Rack-Ansicht
UNIT_WIDTH = 220
UNIT_HEIGHT = 22
LEGEND_WIDTH = 30
MARGIN_WIDTH = 15
def device_image(device_type, face):
"""Bilddatei (FieldFile) des Gerätetyps für die Seite oder None."""
image = getattr(device_type, "front_image" if face == "front" else "rear_image", None)
return image if image else None
def _data_uri(image):
mime = mimetypes.guess_type(image.name)[0] or "image/png"
with image.open("rb") as f:
return f"data:{mime};base64,{base64.b64encode(f.read()).decode('ascii')}"
def _inline_images(svg, devices):
"""Ersetzt die /media/…-Links der Gerätebilder durch eingebettete Daten.
Word, WeasyPrint und
laden keine externen (login-geschützten) Dateien nach.
"""
seen = set()
for d in devices:
for face in ("front", "rear"):
image = device_image(d.device_type, face)
if not image or image.name in seen:
continue
seen.add(image.name)
try:
svg = svg.replace(f'"{image.url}"', f'"{_data_uri(image)}"')
except OSError:
logger.warning("Gerätebild %s nicht lesbar", image.name)
return svg
# Helles NetBox-Theme mit festen Werten. Das Original-Stylesheet nutzt CSS-Variablen (var(--nbx-…)),
# die WeasyPrint und cairosvg nicht auflösen. Leere HE würden sonst schwarz gefüllt.
STATIC_CSS = """
svg { font-family: Helvetica, Arial, sans-serif; font-size: 11px; }
text { text-anchor: middle; dominant-baseline: middle; }
.unit { fill: #6c757d; }
.hidden { visibility: hidden; }
rect.shaded, image.shaded { opacity: .25; }
text.shaded { opacity: .5; }
.rack { fill: none; stroke-width: 2px; stroke: #000000; }
.slot { fill: #e9ecef; stroke: #adb5bd; }
.slot.occupied[class] { fill: url(#occupied); }
.slot.blocked[class] { fill: url(#blocked); }
.reservation[class] { fill: url(#reserved); }
"""
# class -> Präsentationsattribute (Fallback, falls der Renderer ", f"", svg, count=1, flags=re.S)
# "Add Device"-Texte sind nur per Hover sichtbar – im Dokument entfernen
svg = re.sub(r']*class="[^"]*add-device[^"]*"[^>]*>.*?', "", svg, flags=re.S)
return _TAG_WITH_CLASS.sub(_apply_class_attrs, svg)
def render_rack_svg(rack, face, user, images=False, devices=()):
"""Gibt (svg, fehler) zurück – genau einer der beiden Werte ist None.
Ist ``rack_svg_renderer`` gesetzt (z.B. der Offline-Renderer aus Netbox-Utilities, der mehrere
Geräte pro HE korrekt darstellt), wird dieser aufgerufen: ``fn(rack=..., face=..., user=..., images=...)``.
Andernfalls wird NetBox' eigener RackElevationSVG direkt in-process genutzt.
"""
try:
renderer = get_plugin_config("netbox_customer_export", "rack_svg_renderer")
if renderer:
fn = import_string(renderer)
try:
svg = fn(rack=rack, face=face, user=user, images=images)
except TypeError: # ältere Adapter ohne images-Parameter
svg = fn(rack=rack, face=face, user=user)
else:
from dcim.svg import RackElevationSVG
# Maße explizit übergeben: je nach NetBox-Version fehlen die RACK_ELEVATION_DEFAULT_*-Parameter
# in der Konfiguration, dann bricht der Renderer mit AttributeError ab.
svg = RackElevationSVG(
rack,
unit_width=UNIT_WIDTH,
unit_height=UNIT_HEIGHT,
legend_width=LEGEND_WIDTH,
margin_width=MARGIN_WIDTH,
user=user,
include_images=images,
base_url="",
).render(face)
svg = svg.tostring() if hasattr(svg, "tostring") else str(svg)
svg = normalize_svg(svg)
if images:
svg = _inline_images(svg, devices)
return svg, None
except Exception as e: # noqa: BLE001 – ein defektes Rack soll nicht den ganzen Export abbrechen
logger.exception("Rack-SVG für %s (%s) fehlgeschlagen", rack, face)
return None, f"{type(e).__name__}: {e}"