fix(dcim): Clear stale cable profile data on cable deletion
Clear cable_connector and cable_positions when deleting profiled cables. Adds data migration to clean up stale values from earlier versions that failed to clear these fields, preventing validation errors on affected endpoints. Fixes #22737
This commit is contained in:
parent
ee8aaec433
commit
47102330eb
|
|
@ -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),
|
||||
]
|
||||
|
|
@ -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),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue