fix: bind rack integrations to rendered devices

This commit is contained in:
2026-08-13 10:51:59 +02:00
parent 270d821219
commit e42580d651
11 changed files with 357 additions and 119 deletions
+132
View File
@@ -1,15 +1,29 @@
import logging
import re
from functools import wraps
from html import unescape
from importlib import import_module
from django.apps import apps
from django.core.exceptions import ObjectDoesNotExist
from django.db import OperationalError, ProgrammingError
from django.templatetags.static import static
from django.utils.html import escape, json_script
from netbox.plugins import get_plugin_config
from . import __version__
from .rack_width import effective_width_positions
logger = logging.getLogger(__name__)
TOPOLOGY_RACK_ELEVATION_VIEW = "plugins:netbox_topology_views:rack_elevation"
TOPOLOGY_VIEW_PATCH_MARKER = "_netbox_utilities_rack_width_view"
RACK_DEVICE_TAG_RE = re.compile(
r"<a\b[^>]*\bclass=[\"'][^\"']*\brack-device\b[^\"']*[\"'][^>]*>",
re.IGNORECASE,
)
HREF_RE = re.compile(r"\bhref=[\"'](?P<url>[^\"']+)[\"']", re.IGNORECASE)
DEVICE_ID_RE = re.compile(r"(?:^|/)dcim/devices/(?P<device_id>\d+)/?(?:$|[?#])")
def is_topology_rack_elevation_request(request):
@@ -108,3 +122,121 @@ def get_topology_rack_width_data(request):
)
data["status"] = "native-fallback"
return data
def _visible_topology_device_urls(html):
"""Return device URLs which the authorized Topology response actually contains."""
urls = {}
for tag_match in RACK_DEVICE_TAG_RE.finditer(html):
href_match = HREF_RE.search(tag_match.group(0))
if href_match is None:
continue
url = unescape(href_match.group("url"))
device_match = DEVICE_ID_RE.search(url)
if device_match is not None:
urls[int(device_match.group("device_id"))] = url
return urls
def _topology_data_for_visible_devices(visible_urls):
"""Build widths only for devices already exposed by the foreign view."""
from dcim.models import Device
devices = list(
Device.objects.filter(pk__in=visible_urls)
.select_related("device_type", "netbox_utilities_rack_placement")
.order_by("rack_id", "face", "position", "pk")
)
widths = effective_width_positions(devices)
result = {"devices": [], "status": "ready", "schema_version": 3, "complete": True}
for device in devices:
width, position, source = widths[device.pk]
if width <= 1:
continue
descriptor = serialize_topology_width(device, width, position, source=source)
descriptor["url"] = visible_urls[device.pk]
result["devices"].append(descriptor)
return result
def _css_string(value):
return str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\a ")
def _topology_width_head(data):
rules = []
for device in data["devices"]:
rules.append(
f'.rack-device[href="{_css_string(device["url"])}"] {{'
" right: auto !important;"
f" left: calc({device['left_percent']}% + 3px) !important;"
f" width: calc({device['width_percent']}% - 6px) !important;"
" }"
)
stylesheet = f'<style id="netbox-utilities-topology-rack-width-styles">{"".join(rules)}</style>'
payload = json_script(data, "netbox-utilities-topology-rack-width-data")
script_url = escape(f"{static('netbox_utilities/topology-rack-width.js')}?v={__version__}")
return f'{stylesheet}{payload}<script src="{script_url}" defer></script>'
def _inject_topology_widths(response):
if (
getattr(response, "streaming", False)
or getattr(response, "status_code", None) != 200
or "text/html" not in response.get("Content-Type", "")
):
return response
html = response.content.decode(response.charset or "utf-8")
if "</head>" not in html.lower() or "netbox-utilities-topology-rack-width-data" in html:
return response
visible_urls = _visible_topology_device_urls(html)
data = _topology_data_for_visible_devices(visible_urls)
insertion = _topology_width_head(data)
head_end = html.lower().index("</head>")
html = f"{html[:head_end]}{insertion}{html[head_end:]}"
content = html.encode(response.charset or "utf-8")
response.content = content
if response.has_header("Content-Length"):
response["Content-Length"] = str(len(content))
return response
def install_topology_rack_width_support():
"""Patch the optional view and derive widths from its authorized HTML."""
if not (
apps.is_installed("netbox_topology_views")
and get_plugin_config("netbox_utilities", "topology_views_rack_width_enabled")
):
return False
try:
topology_views = import_module("netbox_topology_views.views")
view = topology_views.RackElevationView
except (AttributeError, ImportError):
logger.warning("NetBox Utilities could not find the optional Topology rack elevation view")
return False
if getattr(view, TOPOLOGY_VIEW_PATCH_MARKER, False):
return True
original_get = view.get
@wraps(original_get)
def width_aware_topology_get(self, request, *args, **kwargs):
response = original_get(self, request, *args, **kwargs)
try:
return _inject_topology_widths(response)
except (AttributeError, ObjectDoesNotExist, OperationalError, ProgrammingError, TypeError, ValueError):
logger.warning(
"Could not inject partial rack widths into NetBox Topology Views path %s",
request.get_full_path(),
exc_info=True,
)
return response
view._netbox_utilities_original_get = original_get
view.get = width_aware_topology_get
setattr(view, TOPOLOGY_VIEW_PATCH_MARKER, True)
logger.info("Enabled partial rack widths for the optional Topology rack elevation view")
return True