Ver código fonte

Closes #22104: Avoid retracing paths when deleting Cables (#22167)

Jeremy Stretch 1 semana atrás
pai
commit
b1412514f1

+ 26 - 0
netbox/dcim/models/cables.py

@@ -1,5 +1,6 @@
 import itertools
 import itertools
 import logging
 import logging
+import threading
 from collections import Counter
 from collections import Counter
 
 
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
@@ -76,6 +77,11 @@ class Cable(PrimaryModel):
     """
     """
     A physical connection between two endpoints.
     A physical connection between two endpoints.
     """
     """
+    # Per-thread tracking of Cable PKs currently in delete(); referenced by
+    # dcim.signals.nullify_connected_endpoints to skip per-CableTermination
+    # cable path retracing during cascade (retrace_cable_paths handles it once).
+    _deletion_tracking = threading.local()
+
     type = models.CharField(
     type = models.CharField(
         verbose_name=_('type'),
         verbose_name=_('type'),
         max_length=50,
         max_length=50,
@@ -343,6 +349,26 @@ class Cable(PrimaryModel):
         except UnsupportedCablePath as e:
         except UnsupportedCablePath as e:
             raise AbortRequest(e)
             raise AbortRequest(e)
 
 
+    def delete(self, *args, **kwargs):
+        # Track this Cable as being deleted so the post_delete signal handler
+        # for cascaded CableTerminations can skip redundant path retracing;
+        # retrace_cable_paths() will retrace each affected path once after the
+        # Cable itself is deleted. Cache the PK locally because super().delete()
+        # clears self.pk before the finally block runs. The tracking set lives
+        # on a threading.local() to isolate concurrent deletions across threads.
+        if not hasattr(Cable._deletion_tracking, 'pks'):
+            Cable._deletion_tracking.pks = set()
+        pk = self.pk
+        Cable._deletion_tracking.pks.add(pk)
+        try:
+            return super().delete(*args, **kwargs)
+        finally:
+            Cable._deletion_tracking.pks.discard(pk)
+
+    @classmethod
+    def _is_being_deleted(cls, pk):
+        return pk in getattr(cls._deletion_tracking, 'pks', ())
+
     def clone(self):
     def clone(self):
         """
         """
         Return attributes suitable for cloning this cable.
         Return attributes suitable for cloning this cable.

+ 6 - 0
netbox/dcim/signals.py

@@ -185,6 +185,12 @@ def nullify_connected_endpoints(instance, **kwargs):
     model = instance.termination_type.model_class()
     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='')
 
 
+    # If the parent Cable is being deleted in this same operation, skip the
+    # per-termination retrace; retrace_cable_paths() will retrace each affected
+    # path once after the Cable is deleted.
+    if Cable._is_being_deleted(instance.cable_id):
+        return
+
     for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
     for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
         # Remove the deleted CableTermination if it's one of the path's originating nodes
         # Remove the deleted CableTermination if it's one of the path's originating nodes
         if instance.termination in cablepath.origins:
         if instance.termination in cablepath.origins:

+ 12 - 0
netbox/dcim/tests/test_cablepaths.py

@@ -55,6 +55,18 @@ class LegacyCablePathTestCase(CablePathTestCase):
         # Check that all CablePaths have been deleted
         # Check that all CablePaths have been deleted
         self.assertEqual(CablePath.objects.count(), 0)
         self.assertEqual(CablePath.objects.count(), 0)
 
 
+        # Check that connected interfaces are fully cleaned up
+        interface1.refresh_from_db()
+        interface2.refresh_from_db()
+
+        self.assertIsNone(interface1.cable_id)
+        self.assertEqual(interface1.cable_end, '')
+        self.assertPathIsNotSet(interface1)
+
+        self.assertIsNone(interface2.cable_id)
+        self.assertEqual(interface2.cable_end, '')
+        self.assertPathIsNotSet(interface2)
+
     def test_102_consoleport_to_consoleserverport(self):
     def test_102_consoleport_to_consoleserverport(self):
         """
         """
         [CP1] --C1-- [CSP1]
         [CP1] --C1-- [CSP1]