Quellcode durchsuchen

Merge pull request #22755 from netbox-community/22737-deleting-a-profiled-cable-leaves-stale-connector-metadata-on

Fixes #22737: Clear cached Cable Profile data when deleting Cables
bctiemann vor 5 Tagen
Ursprung
Commit
b6ff8654e0

+ 30 - 0
netbox/circuits/migrations/0058_clear_stale_cable_profile_data.py

@@ -0,0 +1,30 @@
+from django.db import migrations
+from django.db.models import Q
+
+
+def clear_stale_cable_profile_data(apps, schema_editor):
+    """
+    Clear cached cable connector and position data from circuit terminations which no
+    longer have a cable attached. Earlier versions failed to clear these values when a
+    profiled cable was deleted, causing subsequent validation to fail.
+    """
+    CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
+    db_alias = schema_editor.connection.alias
+
+    CircuitTermination.objects.using(db_alias).filter(
+        Q(cable_connector__isnull=False) | Q(cable_positions__isnull=False),
+        cable__isnull=True,
+    ).update(
+        cable_connector=None,
+        cable_positions=None,
+    )
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('circuits', '0057_default_ordering_indexes'),
+    ]
+
+    operations = [
+        migrations.RunPython(clear_stale_cable_profile_data, migrations.RunPython.noop),
+    ]

+ 42 - 0
netbox/dcim/migrations/0240_clear_stale_cable_profile_data.py

@@ -0,0 +1,42 @@
+from django.db import migrations
+from django.db.models import Q
+
+CABLED_MODELS = (
+    'ConsolePort',
+    'ConsoleServerPort',
+    'FrontPort',
+    'Interface',
+    'PowerFeed',
+    'PowerOutlet',
+    'PowerPort',
+    'RearPort',
+)
+
+
+def clear_stale_cable_profile_data(apps, schema_editor):
+    """
+    Clear cached cable connector and position data from endpoints which no longer have
+    a cable attached. Earlier versions failed to clear these values when a profiled
+    cable was deleted, causing subsequent validation of the endpoint to fail.
+    """
+    db_alias = schema_editor.connection.alias
+
+    for model_name in CABLED_MODELS:
+        model = apps.get_model('dcim', model_name)
+        model.objects.using(db_alias).filter(
+            Q(cable_connector__isnull=False) | Q(cable_positions__isnull=False),
+            cable__isnull=True,
+        ).update(
+            cable_connector=None,
+            cable_positions=None,
+        )
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('dcim', '0239_add_portmapping_objectchange'),
+    ]
+
+    operations = [
+        migrations.RunPython(clear_stale_cable_profile_data, migrations.RunPython.noop),
+    ]

+ 6 - 1
netbox/dcim/signals.py

@@ -203,7 +203,12 @@ def nullify_connected_endpoints(instance, **kwargs):
     Disassociate the Cable from the termination object, and retrace any affected CablePaths.
     """
     model = instance.termination_type.model_class()
-    model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
+    model.objects.filter(pk=instance.termination_id).update(
+        cable=None,
+        cable_end='',
+        cable_connector=None,
+        cable_positions=None,
+    )
 
     # If the parent Cable is being deleted in this same operation, skip the
     # per-termination retrace; retrace_cable_paths() will retrace each affected

+ 31 - 1
netbox/dcim/tests/test_signals.py

@@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.test import SimpleTestCase, TestCase
 
 from dcim import signals
-from dcim.choices import CableEndChoices, LinkStatusChoices
+from dcim.choices import CableEndChoices, CableProfileChoices, LinkStatusChoices
 from dcim.models import (
     Cable,
     CablePath,
@@ -275,6 +275,36 @@ class CableSignalTestCase(TestCase):
         self.assertEqual(interface_a.cable_end, '')
         self.assertEqual(interface_b.cable_end, '')
 
+    def test_deleting_profiled_cable_nullifies_endpoints(self):
+        """
+        Deleting a profiled cable must clear the cached connector and position data on both endpoints.
+        """
+        interface_a = Interface.objects.create(device=self.device, name='Interface A')
+        interface_b = Interface.objects.create(device=self.device, name='Interface B')
+        cable = Cable(
+            a_terminations=[interface_a],
+            b_terminations=[interface_b],
+            profile=CableProfileChoices.SINGLE_1C1P,
+        )
+        cable.save()
+
+        # Confirm the profile metadata was cached on both endpoints.
+        interface_a.refresh_from_db()
+        interface_b.refresh_from_db()
+        self.assertEqual(interface_a.cable_connector, 1)
+        self.assertEqual(interface_a.cable_positions, [1])
+        self.assertEqual(interface_b.cable_connector, 1)
+        self.assertEqual(interface_b.cable_positions, [1])
+
+        cable.delete()
+
+        for interface in (interface_a, interface_b):
+            interface.refresh_from_db()
+            self.assertIsNone(interface.cable_id)
+            self.assertEqual(interface.cable_end, '')
+            self.assertIsNone(interface.cable_connector)
+            self.assertIsNone(interface.cable_positions)
+
     def test_deleting_cable_skips_per_termination_retrace(self):
         """
         When a Cable is deleted, nullify_connected_endpoints (post_delete on each