From 66d13292f2f82414c287fa2266dc125dcdd4fb4a Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 6 Aug 2026 16:03:58 +0200 Subject: [PATCH] fix: remove existing patchpanel backfill --- README.md | 20 ++--- netbox_utilities/__init__.py | 2 +- netbox_utilities/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/repair_patchpanel_mappings.py | 15 ---- .../0004_patchpanel_port_mappings.py | 49 +----------- ...005_fix_patchpanel_port_number_mappings.py | 76 +------------------ .../0006_retrace_existing_patchpanel_paths.py | 14 +--- netbox_utilities/patchpanel.py | 18 +---- netbox_utilities/tests/test_patchpanel.py | 13 ++++ pyproject.toml | 2 +- 11 files changed, 30 insertions(+), 179 deletions(-) delete mode 100644 netbox_utilities/management/__init__.py delete mode 100644 netbox_utilities/management/commands/__init__.py delete mode 100644 netbox_utilities/management/commands/repair_patchpanel_mappings.py diff --git a/README.md b/README.md index 2547e74..267149f 100644 --- a/README.md +++ b/README.md @@ -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.2" + "git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.7.3" ``` Alternativ kann hinter dem `@` die vollständige Commit-ID stehen. @@ -147,20 +147,10 @@ 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.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 -``` +Bei Installation oder Update findet kein automatischer Bestandslauf statt. +Vorhandene Patchpanel-Geräte und deren Kabelpfade bleiben unverändert. Die +Automatik greift erst, wenn ein betreffendes Gerät, Modul oder ein Front-/Rearport +anschließend neu angelegt beziehungsweise gespeichert wird. ### Mehrere Geräte mit NetBox Reorder Rack verschieben diff --git a/netbox_utilities/__init__.py b/netbox_utilities/__init__.py index 054facd..248273a 100644 --- a/netbox_utilities/__init__.py +++ b/netbox_utilities/__init__.py @@ -1,6 +1,6 @@ from netbox.plugins import PluginConfig, get_plugin_config -__version__ = "0.7.2" +__version__ = "0.7.3" class NetBoxUtilitiesConfig(PluginConfig): diff --git a/netbox_utilities/management/__init__.py b/netbox_utilities/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/netbox_utilities/management/commands/__init__.py b/netbox_utilities/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/netbox_utilities/management/commands/repair_patchpanel_mappings.py b/netbox_utilities/management/commands/repair_patchpanel_mappings.py deleted file mode 100644 index 72f6c97..0000000 --- a/netbox_utilities/management/commands/repair_patchpanel_mappings.py +++ /dev/null @@ -1,15 +0,0 @@ -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." - ) - ) diff --git a/netbox_utilities/migrations/0004_patchpanel_port_mappings.py b/netbox_utilities/migrations/0004_patchpanel_port_mappings.py index 3e681df..2c46587 100644 --- a/netbox_utilities/migrations/0004_patchpanel_port_mappings.py +++ b/netbox_utilities/migrations/0004_patchpanel_port_mappings.py @@ -1,55 +1,12 @@ -import re - from django.db import migrations -def natural_port_key(port): - parts = tuple( - int(part) if part.isdigit() else part.casefold() - for part in re.split(r"(\d+)", str(port.name)) - ) - return parts, port.pk - - -def synchronize_existing_patchpanels(apps, schema_editor): - Device = apps.get_model("dcim", "Device") - FrontPort = apps.get_model("dcim", "FrontPort") - PortMapping = apps.get_model("dcim", "PortMapping") - RearPort = apps.get_model("dcim", "RearPort") - database = schema_editor.connection.alias - - device_ids = Device.objects.using(database).filter(role__name__iexact="Patchpanel").values_list("pk", flat=True) - for device_id in device_ids.iterator(): - front_ports = sorted( - FrontPort.objects.using(database).filter(device_id=device_id).only("pk", "name"), - key=natural_port_key, - ) - rear_ports = sorted( - RearPort.objects.using(database).filter(device_id=device_id).only("pk", "name"), - key=natural_port_key, - ) - PortMapping.objects.using(database).filter(device_id=device_id).delete() - PortMapping.objects.using(database).bulk_create( - [ - PortMapping( - device_id=device_id, - front_port_id=front_port.pk, - rear_port_id=rear_port.pk, - front_port_position=1, - rear_port_position=1, - ) - for front_port, rear_port in zip(front_ports, rear_ports) - ], - batch_size=1000, - ) - - class Migration(migrations.Migration): dependencies = [ ("dcim", "0237_module_remove_local_context_data"), ("netbox_utilities", "0003_navigationpreference_sidebar_layout"), ] - operations = [ - migrations.RunPython(synchronize_existing_patchpanels, migrations.RunPython.noop), - ] + # Kept in the migration graph for installations which already recorded it. + # Existing devices must not be modified automatically. + operations = [] diff --git a/netbox_utilities/migrations/0005_fix_patchpanel_port_number_mappings.py b/netbox_utilities/migrations/0005_fix_patchpanel_port_number_mappings.py index ac8c0a3..4fbd945 100644 --- a/netbox_utilities/migrations/0005_fix_patchpanel_port_number_mappings.py +++ b/netbox_utilities/migrations/0005_fix_patchpanel_port_number_mappings.py @@ -1,81 +1,11 @@ -import re -from collections import defaultdict - from django.db import migrations -def natural_port_key(port): - parts = tuple( - int(part) if part.isdigit() else part.casefold() - for part in re.split(r"(\d+)", str(port.name)) - ) - return parts, port.pk - - -def port_identifier(port): - name = str(port.name).casefold() - name = re.sub(r"\b(?:front|rear)(?:[\s_-]*port)?(?=\b|\d)", " ", name) - name = re.sub(r"^(?:fp|rp|f|r)(?=\s*[-_.:/]?\s*\d)", "", name) - tokens = re.findall(r"\d+|[^\W\d_]+", name) - identifier = tuple(int(token) if token.isdigit() else token for token in tokens) - return identifier or None - - -def pair_ports(front_ports, rear_ports): - front_by_identifier = defaultdict(list) - rear_by_identifier = defaultdict(list) - for port in front_ports: - if identifier := port_identifier(port): - front_by_identifier[identifier].append(port) - for port in rear_ports: - if identifier := port_identifier(port): - rear_by_identifier[identifier].append(port) - - pairs = [] - for identifier, matching_front_ports in front_by_identifier.items(): - pairs.extend( - zip( - sorted(matching_front_ports, key=natural_port_key), - sorted(rear_by_identifier.get(identifier, []), key=natural_port_key), - ) - ) - return pairs - - -def fix_existing_patchpanel_mappings(apps, schema_editor): - Device = apps.get_model("dcim", "Device") - FrontPort = apps.get_model("dcim", "FrontPort") - PortMapping = apps.get_model("dcim", "PortMapping") - RearPort = apps.get_model("dcim", "RearPort") - database = schema_editor.connection.alias - - device_ids = Device.objects.using(database).filter(role__name__iexact="Patchpanel").values_list("pk", flat=True) - for device_id in device_ids.iterator(): - front_ports = FrontPort.objects.using(database).filter(device_id=device_id).only("pk", "name") - rear_ports = RearPort.objects.using(database).filter(device_id=device_id).only("pk", "name") - pairs = pair_ports(front_ports, rear_ports) - - PortMapping.objects.using(database).filter(device_id=device_id).delete() - PortMapping.objects.using(database).bulk_create( - [ - PortMapping( - device_id=device_id, - front_port_id=front_port.pk, - rear_port_id=rear_port.pk, - front_port_position=1, - rear_port_position=1, - ) - for front_port, rear_port in pairs - ], - batch_size=1000, - ) - - class Migration(migrations.Migration): dependencies = [ ("netbox_utilities", "0004_patchpanel_port_mappings"), ] - operations = [ - migrations.RunPython(fix_existing_patchpanel_mappings, migrations.RunPython.noop), - ] + # Kept in the migration graph for installations which already recorded it. + # Existing devices must not be modified automatically. + operations = [] diff --git a/netbox_utilities/migrations/0006_retrace_existing_patchpanel_paths.py b/netbox_utilities/migrations/0006_retrace_existing_patchpanel_paths.py index fd4522c..662a3f6 100644 --- a/netbox_utilities/migrations/0006_retrace_existing_patchpanel_paths.py +++ b/netbox_utilities/migrations/0006_retrace_existing_patchpanel_paths.py @@ -1,19 +1,11 @@ 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), - ] + # Kept in the migration graph for installations which already recorded it. + # Existing devices and cable paths must not be modified automatically. + operations = [] diff --git a/netbox_utilities/patchpanel.py b/netbox_utilities/patchpanel.py index 6e01846..2ed1893 100644 --- a/netbox_utilities/patchpanel.py +++ b/netbox_utilities/patchpanel.py @@ -75,7 +75,7 @@ def retrace_patchpanel_paths(ports): return retraced -def synchronize_patchpanel(device_id, *, force_retrace=False): +def synchronize_patchpanel(device_id): """Enforce front position 1 -> rear position 1 for a Patchpanel device.""" if not device_id: return 0 @@ -113,26 +113,10 @@ def synchronize_patchpanel(device_id, *, force_retrace=False): for front, rear in pairs ] ) - force_retrace = True - - 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)) diff --git a/netbox_utilities/tests/test_patchpanel.py b/netbox_utilities/tests/test_patchpanel.py index 467ae6a..6f498cc 100644 --- a/netbox_utilities/tests/test_patchpanel.py +++ b/netbox_utilities/tests/test_patchpanel.py @@ -1,3 +1,4 @@ +from importlib import import_module from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -11,6 +12,18 @@ def port(pk, name): class PatchpanelPairingTest(SimpleTestCase): + def test_existing_device_migrations_are_noops(self): + migration_names = ( + "0004_patchpanel_port_mappings", + "0005_fix_patchpanel_port_number_mappings", + "0006_retrace_existing_patchpanel_paths", + ) + + for migration_name in migration_names: + with self.subTest(migration=migration_name): + module = import_module(f"netbox_utilities.migrations.{migration_name}") + self.assertEqual(module.Migration.operations, []) + def test_detects_patchpanel_role_case_insensitively(self): self.assertTrue(is_patchpanel(SimpleNamespace(role=SimpleNamespace(name="Patchpanel")))) self.assertTrue(is_patchpanel(SimpleNamespace(role=SimpleNamespace(name="PATCHPANEL")))) diff --git a/pyproject.toml b/pyproject.toml index ab8a847..de56800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "netbox-utilities" -version = "0.7.2" +version = "0.7.3" description = "Navigation, tenant utilities, patchpanel mapping, bulk uploads, and atomic rack reordering for NetBox 4.6" readme = "README.md" requires-python = ">=3.12"