fix: retrace existing patchpanel cable paths

This commit is contained in:
2026-08-06 15:38:06 +02:00
parent e74d8fba31
commit 8c6c48531e
9 changed files with 119 additions and 22 deletions
+12 -3
View File
@@ -35,7 +35,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden:
```bash
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
"git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.7.1"
"git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.7.2"
```
Alternativ kann hinter dem `@` die vollständige Commit-ID stehen.
@@ -147,12 +147,21 @@ nach dem Anlegen oder Umbenennen einzelner Front- beziehungsweise Rearports
erneut hergestellt. Dabei ersetzt die Patchpanel-Automatik abweichende
vorhandene Port-Mappings auf diesen Geräten durch die feste 1:1-Zuordnung.
Beim Update auf Version `0.7.1` korrigiert eine Plugin-Migration die Zuordnung
automatisch für alle bereits vorhandenen Geräte mit dieser Rolle. Vor dem
Beim Update auf Version `0.7.2` korrigiert eine Plugin-Migration die Zuordnung
automatisch für alle bereits vorhandenen Geräte mit dieser Rolle und berechnet
die betroffenen Kabelpfade beziehungsweise Verbindungsenden neu. Vor dem
Produktivupdate sollte daher geprüft werden, ob Geräte mit bewusst
abweichenden oder mehrpoligen Mappings nicht die Rolle `Patchpanel` tragen
sollen.
Der Reparaturlauf kann bei Bedarf jederzeit erneut und ohne erneute Migration
gestartet werden:
```bash
cd /opt/netbox/netbox
/opt/netbox/venv/bin/python manage.py repair_patchpanel_mappings
```
### Mehrere Geräte mit NetBox Reorder Rack verschieben
Wenn `netbox-reorder-rack` in Version `1.1.4` installiert und aktiviert ist,
+1 -1
View File
@@ -1,6 +1,6 @@
from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.7.1"
__version__ = "0.7.2"
class NetBoxUtilitiesConfig(PluginConfig):
@@ -0,0 +1,15 @@
from django.core.management.base import BaseCommand
from netbox_utilities.patchpanel import repair_existing_patchpanels
class Command(BaseCommand):
help = "Repair front/rear mappings and cable paths for all devices with the Patchpanel role"
def handle(self, *args, **options):
device_count, mapping_count = repair_existing_patchpanels()
self.stdout.write(
self.style.SUCCESS(
f"Repaired {mapping_count} front/rear mappings on {device_count} Patchpanel devices."
)
)
@@ -0,0 +1,19 @@
from django.db import migrations
def repair_mappings_and_paths(apps, schema_editor):
# Runtime models are intentional here: NetBox's CablePath.retrace() is
# required to refresh the denormalized path data after migration 0005.
from netbox_utilities.patchpanel import repair_existing_patchpanels
repair_existing_patchpanels()
class Migration(migrations.Migration):
dependencies = [
("netbox_utilities", "0005_fix_patchpanel_port_number_mappings"),
]
operations = [
migrations.RunPython(repair_mappings_and_paths, migrations.RunPython.noop),
]
+47 -16
View File
@@ -59,7 +59,23 @@ def pair_ports(front_ports, rear_ports):
return sorted(pairs, key=lambda pair: natural_port_key(pair[0]))
def synchronize_patchpanel(device_id):
def retrace_patchpanel_paths(ports):
"""Retrace cable paths touching any of the supplied pass-through ports."""
from dcim.models import CablePath
path_ids = set()
for port in ports:
path_ids.update(CablePath.objects.filter(_nodes__contains=port).values_list("pk", flat=True))
retraced = 0
for path_id in path_ids:
if cable_path := CablePath.objects.filter(pk=path_id).first():
cable_path.retrace()
retraced += 1
return retraced
def synchronize_patchpanel(device_id, *, force_retrace=False):
"""Enforce front position 1 -> rear position 1 for a Patchpanel device."""
if not device_id:
return 0
@@ -83,25 +99,40 @@ def synchronize_patchpanel(device_id):
"rear_port_position",
)
)
if current == expected:
return len(pairs)
if current != expected:
PortMapping.objects.filter(device_id=device_id).delete()
PortMapping.objects.bulk_create(
[
PortMapping(
device_id=device_id,
front_port_id=front.pk,
rear_port_id=rear.pk,
front_port_position=1,
rear_port_position=1,
)
for front, rear in pairs
]
)
force_retrace = True
PortMapping.objects.filter(device_id=device_id).delete()
PortMapping.objects.bulk_create(
[
PortMapping(
device_id=device_id,
front_port_id=front.pk,
rear_port_id=rear.pk,
front_port_position=1,
rear_port_position=1,
)
for front, rear in pairs
]
)
if force_retrace:
retrace_patchpanel_paths([*front_ports, *rear_ports])
return len(pairs)
def repair_existing_patchpanels():
"""Repair mappings and cable paths for all existing Patchpanel devices."""
from dcim.models import Device
device_ids = Device.objects.filter(role__name__iexact=PATCHPANEL_ROLE_NAME).values_list("pk", flat=True)
device_count = 0
mapping_count = 0
for device_id in device_ids:
mapping_count += synchronize_patchpanel(device_id, force_retrace=True)
device_count += 1
return device_count, mapping_count
def _component_saved(sender, instance, raw=False, **kwargs):
if not raw and not _component_instantiation_active.get():
synchronize_patchpanel(getattr(instance, "device_id", None))
+24 -1
View File
@@ -1,8 +1,9 @@
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase
from netbox_utilities.patchpanel import is_patchpanel, pair_ports, port_identifier
from netbox_utilities.patchpanel import is_patchpanel, pair_ports, port_identifier, retrace_patchpanel_paths
def port(pk, name):
@@ -37,3 +38,25 @@ class PatchpanelPairingTest(SimpleTestCase):
pairs = pair_ports([port(1, "Front 1")], [port(102, "Rear 2")])
self.assertEqual(pairs, [])
@patch("dcim.models.CablePath")
def test_retraces_each_affected_cable_path_once(self, cable_path_model):
first_path = MagicMock()
second_path = MagicMock()
def filter_paths(**kwargs):
queryset = MagicMock()
if "_nodes__contains" in kwargs:
port_id = kwargs["_nodes__contains"].pk
queryset.values_list.return_value = [1] if port_id == 1 else [1, 2]
else:
queryset.first.return_value = {1: first_path, 2: second_path}.get(kwargs["pk"])
return queryset
cable_path_model.objects.filter.side_effect = filter_paths
retraced = retrace_patchpanel_paths([port(1, "Front 1"), port(2, "Front 2")])
self.assertEqual(retraced, 2)
first_path.retrace.assert_called_once_with()
second_path.retrace.assert_called_once_with()
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "netbox-utilities"
version = "0.7.1"
version = "0.7.2"
description = "Navigation, tenant utilities, patchpanel mapping, bulk uploads, and atomic rack reordering for NetBox 4.6"
readme = "README.md"
requires-python = ">=3.12"