From 91b51f62d6e641390a2570d768a139f48bf0bf12 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Sat, 25 Jul 2026 13:52:12 +0200 Subject: [PATCH] fix(dcim): Nullify empty cable_end values instead of empty strings Replace empty strings with null values for cable_end fields across cable termination models. Adds data migrations to clean up inconsistent values from earlier versions that wrote empty strings when cables were deleted. Fixes #22768 --- .../0059_nullify_empty_cable_end.py | 23 ++++++++++++ .../0241_nullify_empty_cable_end.py | 35 +++++++++++++++++++ netbox/dcim/signals.py | 2 +- netbox/dcim/tests/test_cablepaths.py | 4 +-- netbox/dcim/tests/test_signals.py | 8 ++--- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 netbox/circuits/migrations/0059_nullify_empty_cable_end.py create mode 100644 netbox/dcim/migrations/0241_nullify_empty_cable_end.py diff --git a/netbox/circuits/migrations/0059_nullify_empty_cable_end.py b/netbox/circuits/migrations/0059_nullify_empty_cable_end.py new file mode 100644 index 000000000..f3ccb758d --- /dev/null +++ b/netbox/circuits/migrations/0059_nullify_empty_cable_end.py @@ -0,0 +1,23 @@ +from django.db import migrations + + +def nullify_empty_cable_end(apps, schema_editor): + """ + Replace empty strings with null values on cached cable end data. Earlier versions + wrote an empty string when a cable termination was deleted, leaving disconnected + terminations inconsistent with those which have never been cabled. + """ + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + db_alias = schema_editor.connection.alias + + CircuitTermination.objects.using(db_alias).filter(cable_end='').update(cable_end=None) + + +class Migration(migrations.Migration): + dependencies = [ + ('circuits', '0058_clear_stale_cable_profile_data'), + ] + + operations = [ + migrations.RunPython(nullify_empty_cable_end, migrations.RunPython.noop), + ] diff --git a/netbox/dcim/migrations/0241_nullify_empty_cable_end.py b/netbox/dcim/migrations/0241_nullify_empty_cable_end.py new file mode 100644 index 000000000..35bab9b52 --- /dev/null +++ b/netbox/dcim/migrations/0241_nullify_empty_cable_end.py @@ -0,0 +1,35 @@ +from django.db import migrations + +CABLED_MODELS = ( + 'ConsolePort', + 'ConsoleServerPort', + 'FrontPort', + 'Interface', + 'PowerFeed', + 'PowerOutlet', + 'PowerPort', + 'RearPort', +) + + +def nullify_empty_cable_end(apps, schema_editor): + """ + Replace empty strings with null values on cached cable end data. Earlier versions + wrote an empty string when a cable termination was deleted, leaving disconnected + endpoints inconsistent with those which have never been cabled. + """ + 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(cable_end='').update(cable_end=None) + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0240_clear_stale_cable_profile_data'), + ] + + operations = [ + migrations.RunPython(nullify_empty_cable_end, migrations.RunPython.noop), + ] diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index f7fb9c1b4..ca0f12512 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -205,7 +205,7 @@ def nullify_connected_endpoints(instance, **kwargs): model = instance.termination_type.model_class() model.objects.filter(pk=instance.termination_id).update( cable=None, - cable_end='', + cable_end=None, cable_connector=None, cable_positions=None, ) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 7fa4f9b8b..d60f5cc86 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -60,11 +60,11 @@ class LegacyCablePathTestCase(BaseCablePathTestCase): interface2.refresh_from_db() self.assertIsNone(interface1.cable_id) - self.assertEqual(interface1.cable_end, '') + self.assertIsNone(interface1.cable_end) self.assertPathIsNotSet(interface1) self.assertIsNone(interface2.cable_id) - self.assertEqual(interface2.cable_end, '') + self.assertIsNone(interface2.cable_end) self.assertPathIsNotSet(interface2) def test_102_consoleport_to_consoleserverport(self): diff --git a/netbox/dcim/tests/test_signals.py b/netbox/dcim/tests/test_signals.py index 99ceff0f0..119b23ecf 100644 --- a/netbox/dcim/tests/test_signals.py +++ b/netbox/dcim/tests/test_signals.py @@ -272,8 +272,8 @@ class CableSignalTestCase(TestCase): self.assertIsNone(interface_b._path_id) self.assertIsNone(interface_a.cable_id) self.assertIsNone(interface_b.cable_id) - self.assertEqual(interface_a.cable_end, '') - self.assertEqual(interface_b.cable_end, '') + self.assertIsNone(interface_a.cable_end) + self.assertIsNone(interface_b.cable_end) def test_deleting_profiled_cable_nullifies_endpoints(self): """ @@ -301,7 +301,7 @@ class CableSignalTestCase(TestCase): 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_end) self.assertIsNone(interface.cable_connector) self.assertIsNone(interface.cable_positions) @@ -367,7 +367,7 @@ class CableSignalTestCase(TestCase): termination.delete() interface_a.refresh_from_db() self.assertIsNone(interface_a.cable_id) - self.assertEqual(interface_a.cable_end, '') + self.assertIsNone(interface_a.cable_end) class MACAddressInterfaceSignalTestCase(TestCase):