fix: retrace existing patchpanel cable paths
This commit is contained in:
@@ -35,7 +35,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
|
/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.
|
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
|
erneut hergestellt. Dabei ersetzt die Patchpanel-Automatik abweichende
|
||||||
vorhandene Port-Mappings auf diesen Geräten durch die feste 1:1-Zuordnung.
|
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
|
Beim Update auf Version `0.7.2` korrigiert eine Plugin-Migration die Zuordnung
|
||||||
automatisch für alle bereits vorhandenen Geräte mit dieser Rolle. Vor dem
|
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
|
Produktivupdate sollte daher geprüft werden, ob Geräte mit bewusst
|
||||||
abweichenden oder mehrpoligen Mappings nicht die Rolle `Patchpanel` tragen
|
abweichenden oder mehrpoligen Mappings nicht die Rolle `Patchpanel` tragen
|
||||||
sollen.
|
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
|
### Mehrere Geräte mit NetBox Reorder Rack verschieben
|
||||||
|
|
||||||
Wenn `netbox-reorder-rack` in Version `1.1.4` installiert und aktiviert ist,
|
Wenn `netbox-reorder-rack` in Version `1.1.4` installiert und aktiviert ist,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from netbox.plugins import PluginConfig, get_plugin_config
|
from netbox.plugins import PluginConfig, get_plugin_config
|
||||||
|
|
||||||
__version__ = "0.7.1"
|
__version__ = "0.7.2"
|
||||||
|
|
||||||
|
|
||||||
class NetBoxUtilitiesConfig(PluginConfig):
|
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),
|
||||||
|
]
|
||||||
@@ -59,7 +59,23 @@ def pair_ports(front_ports, rear_ports):
|
|||||||
return sorted(pairs, key=lambda pair: natural_port_key(pair[0]))
|
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."""
|
"""Enforce front position 1 -> rear position 1 for a Patchpanel device."""
|
||||||
if not device_id:
|
if not device_id:
|
||||||
return 0
|
return 0
|
||||||
@@ -83,25 +99,40 @@ def synchronize_patchpanel(device_id):
|
|||||||
"rear_port_position",
|
"rear_port_position",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if current == expected:
|
if current != expected:
|
||||||
return len(pairs)
|
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()
|
if force_retrace:
|
||||||
PortMapping.objects.bulk_create(
|
retrace_patchpanel_paths([*front_ports, *rear_ports])
|
||||||
[
|
|
||||||
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
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return len(pairs)
|
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):
|
def _component_saved(sender, instance, raw=False, **kwargs):
|
||||||
if not raw and not _component_instantiation_active.get():
|
if not raw and not _component_instantiation_active.get():
|
||||||
synchronize_patchpanel(getattr(instance, "device_id", None))
|
synchronize_patchpanel(getattr(instance, "device_id", None))
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.test import SimpleTestCase
|
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):
|
def port(pk, name):
|
||||||
@@ -37,3 +38,25 @@ class PatchpanelPairingTest(SimpleTestCase):
|
|||||||
pairs = pair_ports([port(1, "Front 1")], [port(102, "Rear 2")])
|
pairs = pair_ports([port(1, "Front 1")], [port(102, "Rear 2")])
|
||||||
|
|
||||||
self.assertEqual(pairs, [])
|
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-utilities"
|
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"
|
description = "Navigation, tenant utilities, patchpanel mapping, bulk uploads, and atomic rack reordering for NetBox 4.6"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
Reference in New Issue
Block a user