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
This commit is contained in:
bctiemann 2026-07-23 14:00:29 -04:00 committed by GitHub
commit b6ff8654e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 109 additions and 2 deletions

View File

@ -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),
]

View File

@ -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),
]

View File

@ -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

View File

@ -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