2
0
Эх сурвалжийг харах

fix(dcim): Rebuild paths when either Cable end is recreated (#23318)

update_terminations() raised _terminations_modified for a forced
recreation only while filling a cold B-end termination cache, so a
profile change saved on a reused Cable instance tore its paths down
and never rebuilt them. Raise it for every forced recreation,
independent of the caches.

Fixes #23307
Martin Hauser 10 цаг өмнө
parent
commit
a6e0fa0376

+ 4 - 3
netbox/dcim/models/cables.py

@@ -573,6 +573,10 @@ class Cable(PrimaryModel):
         force_a = force or self._connectors_reassigned(a_terminations, self.a_terminations)
         force_b = force or self._connectors_reassigned(b_terminations, self.b_terminations)
 
+        # Recreating either end's terminations invalidates its paths, even when the endpoints are unchanged
+        if force_a or force_b:
+            self._terminations_modified = True
+
         # When force-recreating terminations (e.g. after a profile change), cache the termination objects
         # from the database before deleting, so they are available for recreation. Without this, the
         # a_terminations/b_terminations properties would query the DB after deletion and return empty lists.
@@ -581,9 +585,6 @@ class Cable(PrimaryModel):
         if force_b and not hasattr(self, '_b_terminations'):
             self._b_terminations = list(b_terminations.keys())
 
-            # Recreating terminations invalidates existing paths, even when the endpoints are unchanged
-            self._terminations_modified = True
-
         # Delete any stale CableTerminations
         for termination, ct in a_terminations.items():
             if force_a or (termination.pk and termination not in self.a_terminations):

+ 33 - 0
netbox/dcim/tests/test_cablepaths2.py

@@ -2850,3 +2850,36 @@ class CablePathTestCase(BaseCablePathTestCase):
             (interfaces[1], cable2, front_ports[1], rear_ports[1]), is_complete=False
         )
         self.assertEqual(CablePath.objects.count(), 3)
+
+    def test_312_change_cable_profile_on_a_warm_instance_rebuilds_paths(self):
+        """
+        [IF1] --C1-- [IF2]
+
+        Applying a profile to an instance whose termination caches are warm must recreate the rows and the paths.
+        """
+        interfaces = [
+            Interface.objects.create(device=self.device, name='Interface 1'),
+            Interface.objects.create(device=self.device, name='Interface 2'),
+        ]
+
+        # Creating the cable warms both caches and saving it resets the flag
+        cable1 = Cable(
+            a_terminations=[interfaces[0]],
+            b_terminations=[interfaces[1]],
+        )
+        cable1.clean()
+        cable1.save()
+        termination_pks = set(CableTermination.objects.filter(cable=cable1).values_list('pk', flat=True))
+        self.assertEqual(CablePath.objects.count(), 2)
+        self.assertTrue(hasattr(cable1, '_a_terminations') and hasattr(cable1, '_b_terminations'))
+        self.assertFalse(cable1._terminations_modified)
+
+        cable1.profile = CableProfileChoices.SINGLE_1C1P
+        cable1.full_clean()
+        cable1.save()
+
+        self.assertCurrentPathExists((interfaces[0], cable1, interfaces[1]), is_complete=True, is_active=True)
+        self.assertCurrentPathExists((interfaces[1], cable1, interfaces[0]), is_complete=True, is_active=True)
+        self.assertEqual(CablePath.objects.count(), 2)
+        new_termination_pks = set(CableTermination.objects.filter(cable=cable1).values_list('pk', flat=True))
+        self.assertTrue(termination_pks.isdisjoint(new_termination_pks))