From 144f23444b2b27878fd0805b0b0a0046ec7dc63c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 12 Dec 2025 10:26:01 -0500 Subject: [PATCH] Cleanup; updated tests --- netbox/circuits/filtersets.py | 2 +- netbox/circuits/tests/test_filtersets.py | 2 +- netbox/dcim/api/serializers_/cables.py | 4 +- netbox/dcim/cable_profiles.py | 53 +++++++++++++----------- netbox/dcim/filtersets.py | 16 +++++-- netbox/dcim/models/cables.py | 2 +- netbox/dcim/models/device_components.py | 4 +- netbox/dcim/tests/test_api.py | 2 +- netbox/dcim/tests/test_filtersets.py | 9 +++- 9 files changed, 56 insertions(+), 38 deletions(-) diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index c98defd2f..6c7b45164 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -353,7 +353,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet): model = CircuitTermination fields = ( 'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', - 'mark_connected', 'pp_info', 'cable_end', + 'mark_connected', 'pp_info', 'cable_end', 'cable_connector', ) def search(self, queryset, name, value): diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index 91077ee64..6b6a93608 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CircuitTermination.objects.all() filterset = CircuitTerminationFilterSet - ignore_fields = ('cable',) + ignore_fields = ('cable', 'cable_positions') @classmethod def setUpTestData(cls): diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py index 0effbd536..ff648839b 100644 --- a/netbox/dcim/api/serializers_/cables.py +++ b/netbox/dcim/api/serializers_/cables.py @@ -61,11 +61,11 @@ class CableTerminationSerializer(NetBoxModelSerializer): model = CableTermination fields = [ 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', - 'termination', 'position', 'created', 'last_updated', + 'termination', 'positions', 'created', 'last_updated', ] read_only_fields = fields brief_fields = ( - 'id', 'url', 'display', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id', + 'id', 'url', 'display', 'cable', 'cable_end', 'positions', 'termination_type', 'termination_id', ) diff --git a/netbox/dcim/cable_profiles.py b/netbox/dcim/cable_profiles.py index 14200cca2..3e7f24f60 100644 --- a/netbox/dcim/cable_profiles.py +++ b/netbox/dcim/cable_profiles.py @@ -1,29 +1,37 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + from dcim.models import CableTermination class BaseCableProfile: def clean(self, cable): - pass - # # Enforce maximum connection limits - # if self.a_max_connections and len(cable.a_terminations) > self.a_max_connections: - # raise ValidationError({ - # 'a_terminations': _( - # 'Maximum A side connections for profile {profile}: {max}' - # ).format( - # profile=cable.get_profile_display(), - # max=self.a_max_connections, - # ) - # }) - # if self.b_max_connections and len(cable.b_terminations) > self.b_max_connections: - # raise ValidationError({ - # 'b_terminations': _( - # 'Maximum B side connections for profile {profile}: {max}' - # ).format( - # profile=cable.get_profile_display(), - # max=self.b_max_connections, - # ) - # }) + # Enforce maximum terminations limits + a_terminations_count = len(cable.a_terminations) + b_terminations_count = len(cable.b_terminations) + max_a_terminations = len(self.a_connectors) + max_b_terminations = len(self.b_connectors) + if a_terminations_count > max_a_terminations: + raise ValidationError({ + 'a_terminations': _( + 'A side of cable has {count} terminations but only {max} are permitted for profile {profile}' + ).format( + count=a_terminations_count, + profile=cable.get_profile_display(), + max=max_a_terminations, + ) + }) + if b_terminations_count > max_b_terminations: + raise ValidationError({ + 'b_terminations': _( + 'B side of cable has {count} terminations but only {max} are permitted for profile {profile}' + ).format( + count=b_terminations_count, + profile=cable.get_profile_display(), + max=max_b_terminations, + ) + }) def get_mapped_position(self, side, connector, position): """ @@ -36,14 +44,11 @@ class BaseCableProfile: """ Given a terminating object, return the peer terminating object (if any) on the opposite end of the cable. """ - print(f'get_peer_termination({termination}, {position})') - print(f' Mapping {termination.cable_end} {termination.cable_connector}:{position}...') connector, position = self.get_mapped_position( termination.cable_end, termination.cable_connector, position ) - print(f' Mapped to {connector}:{position}') try: ct = CableTermination.objects.get( cable=termination.cable, @@ -51,10 +56,8 @@ class BaseCableProfile: connector=connector, positions__contains=[position], ) - print(f' Found termination {ct.termination}') return ct.termination, position except CableTermination.DoesNotExist: - print(f' Failed to resolve far end termination for {connector}:{position}') return None, None diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index ef5527418..24ce1fea3 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1748,7 +1748,9 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe class Meta: model = ConsolePort - fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end') + fields = ( + 'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector', + ) @register_filterset @@ -1760,7 +1762,9 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi class Meta: model = ConsoleServerPort - fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end') + fields = ( + 'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector', + ) @register_filterset @@ -1774,6 +1778,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, model = PowerPort fields = ( 'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end', + 'cable_connector', ) @@ -1800,6 +1805,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe model = PowerOutlet fields = ( 'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end', + 'cable_connector', ) @@ -2109,7 +2115,7 @@ class InterfaceFilterSet( fields = ( 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', - 'cable_id', 'cable_end', + 'cable_id', 'cable_end', 'cable_connector', ) def filter_virtual_chassis_member_or_master(self, queryset, name, value): @@ -2165,6 +2171,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet) model = FrontPort fields = ( 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', + 'cable_connector', ) @@ -2185,6 +2192,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet): model = RearPort fields = ( 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', + 'cable_connector', ) @@ -2659,7 +2667,7 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo model = PowerFeed fields = ( 'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', - 'available_power', 'mark_connected', 'cable_end', 'description', + 'available_power', 'mark_connected', 'cable_end', 'cable_connector', 'description', ) def search(self, queryset, name, value): diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 823198908..cb1ddd583 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -532,7 +532,7 @@ class CableTermination(ChangeLoggedModel): termination.cable = None termination.cable_end = None termination.cable_connector = None - termination.cable_position = None + termination.cable_positions = None termination.save() super().delete(*args, **kwargs) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 622380e30..f3dd6f28d 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -224,9 +224,9 @@ class CabledObjectModel(models.Model): raise ValidationError({ "cable_end": _("Cable end must not be set without a cable.") }) - if self.cable_position and not self.cable: + if self.cable_positions and not self.cable: raise ValidationError({ - "cable_position": _("Cable termination position must not be set without a cable.") + "cable_positions": _("Cable termination positions must not be set without a cable.") }) if self.mark_connected and self.cable: raise ValidationError({ diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index a76e9563a..1f07368f9 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2620,7 +2620,7 @@ class CableTerminationTest( APIViewTestCases.ListObjectsViewTestCase, ): model = CableTermination - brief_fields = ['cable', 'cable_end', 'display', 'id', 'position', 'termination_id', 'termination_type', 'url'] + brief_fields = ['cable', 'cable_end', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url'] @classmethod def setUpTestData(cls): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 51353b9f3..96a0f14fb 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -3332,6 +3332,7 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests): class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = ConsolePort.objects.all() filterset = ConsolePortFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls): @@ -3582,6 +3583,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = ConsoleServerPort.objects.all() filterset = ConsoleServerPortFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls): @@ -3832,6 +3834,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = PowerPort.objects.all() filterset = PowerPortFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls): @@ -4096,6 +4099,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = PowerOutlet.objects.all() filterset = PowerOutletFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls): @@ -4380,7 +4384,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = Interface.objects.all() filterset = InterfaceFilterSet - ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs') + ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs', 'cable_positions') @classmethod def setUpTestData(cls): @@ -5017,6 +5021,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = FrontPort.objects.all() filterset = FrontPortFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls): @@ -5321,6 +5326,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = RearPort.objects.all() filterset = RearPortFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls): @@ -6859,6 +6865,7 @@ class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests): class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerFeed.objects.all() filterset = PowerFeedFilterSet + ignore_fields = ('cable_positions',) @classmethod def setUpTestData(cls):