From 875e3e7979609f9ac0aa5a3662a154c83b098ea7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 15 Dec 2025 15:41:07 -0500 Subject: [PATCH] Additional work for FR #20788 (#20973) --- netbox/circuits/filtersets.py | 2 +- .../0054_cable_connector_positions.py | 39 + .../migrations/0054_cable_position.py | 23 - ...055_add_comments_to_organizationalmodel.py | 2 +- netbox/circuits/tests/test_filtersets.py | 2 +- netbox/dcim/api/serializers_/cables.py | 5 +- netbox/dcim/cable_profiles.py | 424 ++++++-- netbox/dcim/choices.py | 74 +- netbox/dcim/constants.py | 3 + netbox/dcim/filtersets.py | 22 +- netbox/dcim/migrations/0220_cable_profile.py | 30 +- .../0221_cable_connector_positions.py | 228 ++++ netbox/dcim/migrations/0221_cable_position.py | 107 -- netbox/dcim/migrations/0222_port_mappings.py | 2 +- netbox/dcim/models/cables.py | 99 +- netbox/dcim/models/device_components.py | 71 +- netbox/dcim/tests/test_api.py | 8 +- netbox/dcim/tests/test_cablepaths2.py | 972 +++++++++++++----- netbox/dcim/tests/test_filtersets.py | 9 +- netbox/dcim/utils.py | 6 +- 20 files changed, 1611 insertions(+), 517 deletions(-) create mode 100644 netbox/circuits/migrations/0054_cable_connector_positions.py delete mode 100644 netbox/circuits/migrations/0054_cable_position.py create mode 100644 netbox/dcim/migrations/0221_cable_connector_positions.py delete mode 100644 netbox/dcim/migrations/0221_cable_position.py diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index faf29584f..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', 'cable_position', + 'mark_connected', 'pp_info', 'cable_end', 'cable_connector', ) def search(self, queryset, name, value): diff --git a/netbox/circuits/migrations/0054_cable_connector_positions.py b/netbox/circuits/migrations/0054_cable_connector_positions.py new file mode 100644 index 000000000..12e0e2fbf --- /dev/null +++ b/netbox/circuits/migrations/0054_cable_connector_positions.py @@ -0,0 +1,39 @@ +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('circuits', '0053_owner'), + ] + + operations = [ + migrations.AddField( + model_name='circuittermination', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='circuittermination', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + ] diff --git a/netbox/circuits/migrations/0054_cable_position.py b/netbox/circuits/migrations/0054_cable_position.py deleted file mode 100644 index cedc8813b..000000000 --- a/netbox/circuits/migrations/0054_cable_position.py +++ /dev/null @@ -1,23 +0,0 @@ -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('circuits', '0053_owner'), - ] - - operations = [ - migrations.AddField( - model_name='circuittermination', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - ] diff --git a/netbox/circuits/migrations/0055_add_comments_to_organizationalmodel.py b/netbox/circuits/migrations/0055_add_comments_to_organizationalmodel.py index 5e67aabc0..83574b03b 100644 --- a/netbox/circuits/migrations/0055_add_comments_to_organizationalmodel.py +++ b/netbox/circuits/migrations/0055_add_comments_to_organizationalmodel.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('circuits', '0054_cable_position'), + ('circuits', '0054_cable_connector_positions'), ] operations = [ 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..0ed0cf851 100644 --- a/netbox/dcim/api/serializers_/cables.py +++ b/netbox/dcim/api/serializers_/cables.py @@ -61,11 +61,12 @@ class CableTerminationSerializer(NetBoxModelSerializer): model = CableTermination fields = [ 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', - 'termination', 'position', 'created', 'last_updated', + 'termination', 'connector', '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', 'connector', 'positions', 'termination_type', + 'termination_id', ) diff --git a/netbox/dcim/cable_profiles.py b/netbox/dcim/cable_profiles.py index 4251cd4d9..8d5e787a3 100644 --- a/netbox/dcim/cable_profiles.py +++ b/netbox/dcim/cable_profiles.py @@ -1,108 +1,390 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from dcim.choices import CableEndChoices from dcim.models import CableTermination class BaseCableProfile: - # Maximum number of terminations allowed per side - a_max_connections = None - b_max_connections = None + """Base class for representing a cable profile.""" + + # Mappings of connectors to the number of positions presented by each, at either end of the cable. For example, a + # 12-strand MPO fiber cable would have one connector at either end with six positions (six bidirectional fiber + # pairs). + a_connectors = {} + b_connectors = {} + + # Defined a mapping of A/B connector & position pairings. If not defined, all positions are presumed to be + # symmetrical (i.e. 1:1 on side A maps to 1:1 on side B). If defined, it must be constructed as a dictionary of + # two-item tuples, e.g. {(1, 1): (1, 1)}. + _mapping = None def clean(self, cable): - # Enforce maximum connection limits - if self.a_max_connections and len(cable.a_terminations) > self.a_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': _( - 'Maximum A side connections for profile {profile}: {max}' + '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=self.a_max_connections, + max=max_a_terminations, ) }) - if self.b_max_connections and len(cable.b_terminations) > self.b_max_connections: + if b_terminations_count > max_b_terminations: raise ValidationError({ 'b_terminations': _( - 'Maximum B side connections for profile {profile}: {max}' + '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=self.b_max_connections, + max=max_b_terminations, ) }) - def get_mapped_position(self, side, position): + def get_mapped_position(self, side, connector, position): """ - Return the mapped position for a given cable end and position. - - By default, assume all positions are symmetrical. + Return the mapped far-end connector & position for a given cable end the local connector & position. """ - return position + # By default, assume all positions are symmetrical. + if self._mapping: + return self._mapping.get((connector, position)) + return connector, position - def get_peer_terminations(self, terminations, position_stack): - local_end = terminations[0].cable_end - qs = CableTermination.objects.filter( - cable=terminations[0].cable, - cable_end=terminations[0].opposite_cable_end - ) + def get_peer_termination(self, termination, position): + """ + Given a terminating object, return the peer terminating object (if any) on the opposite end of the cable. + """ + try: + connector, position = self.get_mapped_position( + termination.cable_end, + termination.cable_connector, + position + ) + except TypeError: + raise ValueError( + f"Could not map connector {termination.cable_connector} position {position} on side " + f"{termination.cable_end}" + ) + try: + ct = CableTermination.objects.get( + cable=termination.cable, + cable_end=termination.opposite_cable_end, + connector=connector, + positions__contains=[position], + ) + return ct.termination, position + except CableTermination.DoesNotExist: + return None, None - # TODO: Optimize this to use a single query under any condition - if position_stack: - # Attempt to find a peer termination at the same position currently in the stack. Pop the stack only if - # we find one. Otherwise, return any peer terminations with a null position. - position = self.get_mapped_position(local_end, position_stack[-1][0]) - if peers := qs.filter(position=position): - position_stack.pop() - return peers - - return qs.filter(position=None) + @staticmethod + def get_position_list(n): + """Return a list of integers from 1 to n, inclusive.""" + return list(range(1, n + 1)) -class StraightSingleCableProfile(BaseCableProfile): - a_max_connections = 1 - b_max_connections = 1 +# Profile naming: +# - Single: One connector per side, with one or more positions +# - Trunk: Two or more connectors per side, with one or more positions per connector +# - Breakout: One or more connectors on the A side which map to a greater number of B side connectors +# - Shuffle: A cable with nonlinear position mappings between sides - -class StraightMultiCableProfile(BaseCableProfile): - a_max_connections = None - b_max_connections = None - - -class Shuffle2x2MPO8CableProfile(BaseCableProfile): - a_max_connections = 8 - b_max_connections = 8 - _mapping = { +class Single1C1PCableProfile(BaseCableProfile): + a_connectors = { 1: 1, + } + b_connectors = a_connectors + + +class Single1C2PCableProfile(BaseCableProfile): + a_connectors = { + 1: 2, + } + b_connectors = a_connectors + + +class Single1C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + } + b_connectors = a_connectors + + +class Single1C6PCableProfile(BaseCableProfile): + a_connectors = { + 1: 6, + } + b_connectors = a_connectors + + +class Single1C8PCableProfile(BaseCableProfile): + a_connectors = { + 1: 8, + } + b_connectors = a_connectors + + +class Single1C12PCableProfile(BaseCableProfile): + a_connectors = { + 1: 12, + } + b_connectors = a_connectors + + +class Single1C16PCableProfile(BaseCableProfile): + a_connectors = { + 1: 16, + } + b_connectors = a_connectors + + +class Trunk2C1PCableProfile(BaseCableProfile): + a_connectors = { + 1: 1, + 2: 1, + } + b_connectors = a_connectors + + +class Trunk2C2PCableProfile(BaseCableProfile): + a_connectors = { + 1: 2, 2: 2, - 3: 5, - 4: 6, - 5: 3, - 6: 4, - 7: 7, - 8: 8, } - - def get_mapped_position(self, side, position): - return self._mapping.get(position) + b_connectors = a_connectors -class Shuffle4x4MPO8CableProfile(BaseCableProfile): - a_max_connections = 8 - b_max_connections = 8 - # A side to B side position mapping - _a_mapping = { +class Trunk2C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + 2: 4, + } + b_connectors = a_connectors + + +class Trunk2C6PCableProfile(BaseCableProfile): + a_connectors = { + 1: 6, + 2: 6, + } + b_connectors = a_connectors + + +class Trunk2C8PCableProfile(BaseCableProfile): + a_connectors = { + 1: 8, + 2: 8, + } + b_connectors = a_connectors + + +class Trunk2C12PCableProfile(BaseCableProfile): + a_connectors = { + 1: 12, + 2: 12, + } + b_connectors = a_connectors + + +class Trunk4C1PCableProfile(BaseCableProfile): + a_connectors = { 1: 1, - 2: 3, - 3: 5, - 4: 7, - 5: 2, - 6: 4, - 7: 6, - 8: 8, + 2: 1, + 3: 1, + 4: 1, } - # B side to A side position mapping (reverse of _a_mapping) - _b_mapping = {v: k for k, v in _a_mapping.items()} + b_connectors = a_connectors - def get_mapped_position(self, side, position): - if side.lower() == 'b': - return self._b_mapping.get(position) - return self._a_mapping.get(position) + +class Trunk4C2PCableProfile(BaseCableProfile): + a_connectors = { + 1: 2, + 2: 2, + 3: 2, + 4: 2, + } + b_connectors = a_connectors + + +class Trunk4C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + 2: 4, + 3: 4, + 4: 4, + } + b_connectors = a_connectors + + +class Trunk4C6PCableProfile(BaseCableProfile): + a_connectors = { + 1: 6, + 2: 6, + 3: 6, + 4: 6, + } + b_connectors = a_connectors + + +class Trunk4C8PCableProfile(BaseCableProfile): + a_connectors = { + 1: 8, + 2: 8, + 3: 8, + 4: 8, + } + b_connectors = a_connectors + + +class Trunk8C4PCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + 2: 4, + 3: 4, + 4: 4, + 5: 4, + 6: 4, + 7: 4, + 8: 4, + } + b_connectors = a_connectors + + +class Breakout1C4Px4C1PCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + } + b_connectors = { + 1: 1, + 2: 1, + 3: 1, + 4: 1, + } + _mapping = { + (1, 1): (1, 1), + (1, 2): (2, 1), + (1, 3): (3, 1), + (1, 4): (4, 1), + (2, 1): (1, 2), + (3, 1): (1, 3), + (4, 1): (1, 4), + } + + +class Breakout1C6Px6C1PCableProfile(BaseCableProfile): + a_connectors = { + 1: 6, + } + b_connectors = { + 1: 1, + 2: 1, + 3: 1, + 4: 1, + 5: 1, + 6: 1, + } + _mapping = { + (1, 1): (1, 1), + (1, 2): (2, 1), + (1, 3): (3, 1), + (1, 4): (4, 1), + (1, 5): (5, 1), + (1, 6): (6, 1), + (2, 1): (1, 2), + (3, 1): (1, 3), + (4, 1): (1, 4), + (5, 1): (1, 5), + (6, 1): (1, 6), + } + + +class Trunk2C4PShuffleCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + 2: 4, + } + b_connectors = a_connectors + _mapping = { + (1, 1): (1, 1), + (1, 2): (1, 2), + (1, 3): (2, 1), + (1, 4): (2, 2), + (2, 1): (1, 3), + (2, 2): (1, 4), + (2, 3): (2, 3), + (2, 4): (2, 4), + } + + +class Trunk4C4PShuffleCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + 2: 4, + 3: 4, + 4: 4, + } + b_connectors = a_connectors + _mapping = { + (1, 1): (1, 1), + (1, 2): (2, 1), + (1, 3): (3, 1), + (1, 4): (4, 1), + (2, 1): (1, 2), + (2, 2): (2, 2), + (2, 3): (3, 2), + (2, 4): (4, 2), + (3, 1): (1, 3), + (3, 2): (2, 3), + (3, 3): (3, 3), + (3, 4): (4, 3), + (4, 1): (1, 4), + (4, 2): (2, 4), + (4, 3): (3, 4), + (4, 4): (4, 4), + } + + +class Breakout2C4Px8C1PShuffleCableProfile(BaseCableProfile): + a_connectors = { + 1: 4, + 2: 4, + } + b_connectors = { + 1: 1, + 2: 1, + 3: 1, + 4: 1, + 5: 1, + 6: 1, + 7: 1, + 8: 1, + } + _a_mapping = { + (1, 1): (1, 1), + (1, 2): (2, 1), + (1, 3): (5, 1), + (1, 4): (6, 1), + (2, 1): (3, 1), + (2, 2): (4, 1), + (2, 3): (7, 1), + (2, 4): (8, 1), + } + _b_mapping = { + (1, 1): (1, 1), + (2, 1): (1, 2), + (3, 1): (2, 1), + (4, 1): (2, 2), + (5, 1): (1, 3), + (6, 1): (1, 4), + (7, 1): (2, 3), + (8, 1): (2, 4), + } + + def get_mapped_position(self, side, connector, position): + if side.upper() == CableEndChoices.SIDE_A: + return self._a_mapping.get((connector, position)) + return self._b_mapping.get((connector, position)) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2aecb0907..1be3f7ca4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1722,16 +1722,74 @@ class PortTypeChoices(ChoiceSet): # class CableProfileChoices(ChoiceSet): - STRAIGHT_SINGLE = 'straight-single' - STRAIGHT_MULTI = 'straight-multi' - SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8' - SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8' + # Singles + SINGLE_1C1P = 'single-1c1p' + SINGLE_1C2P = 'single-1c2p' + SINGLE_1C4P = 'single-1c4p' + SINGLE_1C6P = 'single-1c6p' + SINGLE_1C8P = 'single-1c8p' + SINGLE_1C12P = 'single-1c12p' + SINGLE_1C16P = 'single-1c16p' + # Trunks + TRUNK_2C1P = 'trunk-2c1p' + TRUNK_2C2P = 'trunk-2c2p' + TRUNK_2C4P = 'trunk-2c4p' + TRUNK_2C4P_SHUFFLE = 'trunk-2c4p-shuffle' + TRUNK_2C6P = 'trunk-2c6p' + TRUNK_2C8P = 'trunk-2c8p' + TRUNK_2C12P = 'trunk-2c12p' + TRUNK_4C1P = 'trunk-4c1p' + TRUNK_4C2P = 'trunk-4c2p' + TRUNK_4C4P = 'trunk-4c4p' + TRUNK_4C4P_SHUFFLE = 'trunk-4c4p-shuffle' + TRUNK_4C6P = 'trunk-4c6p' + TRUNK_4C8P = 'trunk-4c8p' + TRUNK_8C4P = 'trunk-8c4p' + # Breakouts + BREAKOUT_1C4P_4C1P = 'breakout-1c4p-4c1p' + BREAKOUT_1C6P_6C1P = 'breakout-1c6p-6c1p' + BREAKOUT_2C4P_8C1P_SHUFFLE = 'breakout-2c4p-8c1p-shuffle' CHOICES = ( - (STRAIGHT_SINGLE, _('Straight (single position)')), - (STRAIGHT_MULTI, _('Straight (multi-position)')), - (SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')), - (SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')), + ( + _('Single'), + ( + (SINGLE_1C1P, _('1C1P')), + (SINGLE_1C2P, _('1C2P')), + (SINGLE_1C4P, _('1C4P')), + (SINGLE_1C6P, _('1C6P')), + (SINGLE_1C8P, _('1C8P')), + (SINGLE_1C12P, _('1C12P')), + (SINGLE_1C16P, _('1C16P')), + ), + ), + ( + _('Trunk'), + ( + (TRUNK_2C1P, _('2C1P trunk')), + (TRUNK_2C2P, _('2C2P trunk')), + (TRUNK_2C4P, _('2C4P trunk')), + (TRUNK_2C4P_SHUFFLE, _('2C4P trunk (shuffle)')), + (TRUNK_2C6P, _('2C6P trunk')), + (TRUNK_2C8P, _('2C8P trunk')), + (TRUNK_2C12P, _('2C12P trunk')), + (TRUNK_4C1P, _('4C1P trunk')), + (TRUNK_4C2P, _('4C2P trunk')), + (TRUNK_4C4P, _('4C4P trunk')), + (TRUNK_4C4P_SHUFFLE, _('4C4P trunk (shuffle)')), + (TRUNK_4C6P, _('4C6P trunk')), + (TRUNK_4C8P, _('4C8P trunk')), + (TRUNK_8C4P, _('8C4P trunk')), + ), + ), + ( + _('Breakout'), + ( + (BREAKOUT_1C4P_4C1P, _('1C4P:4C1P breakout')), + (BREAKOUT_1C6P_6C1P, _('1C6P:6C1P breakout')), + (BREAKOUT_2C4P_8C1P_SHUFFLE, _('2C4P:8C1P breakout (shuffle)')), + ), + ), ) diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 16926081f..669345d7c 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -24,6 +24,9 @@ RACK_STARTING_UNIT_DEFAULT = 1 # Cables # +CABLE_CONNECTOR_MIN = 1 +CABLE_CONNECTOR_MAX = 256 + CABLE_POSITION_MIN = 1 CABLE_POSITION_MAX = 1024 diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index cf37b6802..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', 'cable_position') + 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', 'cable_position') + fields = ( + 'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector', + ) @register_filterset @@ -1774,7 +1778,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, model = PowerPort fields = ( 'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end', - 'cable_position', + 'cable_connector', ) @@ -1801,7 +1805,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe model = PowerOutlet fields = ( 'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end', - 'cable_position', + 'cable_connector', ) @@ -2111,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_position', + 'cable_id', 'cable_end', 'cable_connector', ) def filter_virtual_chassis_member_or_master(self, queryset, name, value): @@ -2167,7 +2171,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet) model = FrontPort fields = ( 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', - 'cable_position', + 'cable_connector', ) @@ -2188,7 +2192,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet): model = RearPort fields = ( 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', - 'cable_position', + 'cable_connector', ) @@ -2544,7 +2548,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet): class Meta: model = CableTermination - fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id') + fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id') @register_filterset @@ -2663,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', 'cable_position', 'description', + 'available_power', 'mark_connected', 'cable_end', 'cable_connector', 'description', ) def search(self, queryset, name, value): diff --git a/netbox/dcim/migrations/0220_cable_profile.py b/netbox/dcim/migrations/0220_cable_profile.py index d97ae8df8..5160506ed 100644 --- a/netbox/dcim/migrations/0220_cable_profile.py +++ b/netbox/dcim/migrations/0220_cable_profile.py @@ -1,3 +1,4 @@ +import django.contrib.postgres.fields import django.core.validators from django.db import migrations, models @@ -16,25 +17,40 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='cabletermination', - name='position', - field=models.PositiveIntegerField( + name='connector', + field=models.PositiveSmallIntegerField( blank=True, null=True, validators=[ django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], + django.core.validators.MaxValueValidator(256) + ] + ), + ), + migrations.AddField( + model_name='cabletermination', + name='positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024) + ] + ), + blank=True, + null=True, + size=None ), ), migrations.AlterModelOptions( name='cabletermination', - options={'ordering': ('cable', 'cable_end', 'position', 'pk')}, + options={'ordering': ('cable', 'cable_end', 'connector', 'pk')}, # connector may be null ), migrations.AddConstraint( model_name='cabletermination', constraint=models.UniqueConstraint( - fields=('cable', 'cable_end', 'position'), - name='dcim_cabletermination_unique_position' + fields=('cable', 'cable_end', 'connector'), + name='dcim_cabletermination_unique_connector' ), ), ] diff --git a/netbox/dcim/migrations/0221_cable_connector_positions.py b/netbox/dcim/migrations/0221_cable_connector_positions.py new file mode 100644 index 000000000..e986a28e1 --- /dev/null +++ b/netbox/dcim/migrations/0221_cable_connector_positions.py @@ -0,0 +1,228 @@ +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0220_cable_profile'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='consoleport', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='consoleserverport', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='consoleserverport', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='frontport', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='frontport', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='interface', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='interface', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='powerfeed', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='powerfeed', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='poweroutlet', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='poweroutlet', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='powerport', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='powerport', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name='rearport', + name='cable_connector', + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(256) + ], + ), + ), + migrations.AddField( + model_name='rearport', + name='cable_positions', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + ] diff --git a/netbox/dcim/migrations/0221_cable_position.py b/netbox/dcim/migrations/0221_cable_position.py deleted file mode 100644 index 0c6a2ce5d..000000000 --- a/netbox/dcim/migrations/0221_cable_position.py +++ /dev/null @@ -1,107 +0,0 @@ -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('dcim', '0220_cable_profile'), - ] - - operations = [ - migrations.AddField( - model_name='consoleport', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='consoleserverport', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='frontport', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='interface', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='powerfeed', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='poweroutlet', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='powerport', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - migrations.AddField( - model_name='rearport', - name='cable_position', - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1024), - ], - ), - ), - ] diff --git a/netbox/dcim/migrations/0222_port_mappings.py b/netbox/dcim/migrations/0222_port_mappings.py index a163ae18d..42de44dbc 100644 --- a/netbox/dcim/migrations/0222_port_mappings.py +++ b/netbox/dcim/migrations/0222_port_mappings.py @@ -59,7 +59,7 @@ def populate_port_mappings(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0221_cable_position'), + ('dcim', '0221_cable_connector_positions'), ] operations = [ diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index e75b4c110..8e155d70e 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -3,6 +3,7 @@ import logging from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -136,10 +137,30 @@ class Cable(PrimaryModel): def profile_class(self): from dcim import cable_profiles return { - CableProfileChoices.STRAIGHT_SINGLE: cable_profiles.StraightSingleCableProfile, - CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile, - CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile, - CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile, + CableProfileChoices.SINGLE_1C1P: cable_profiles.Single1C1PCableProfile, + CableProfileChoices.SINGLE_1C2P: cable_profiles.Single1C2PCableProfile, + CableProfileChoices.SINGLE_1C4P: cable_profiles.Single1C4PCableProfile, + CableProfileChoices.SINGLE_1C6P: cable_profiles.Single1C6PCableProfile, + CableProfileChoices.SINGLE_1C8P: cable_profiles.Single1C8PCableProfile, + CableProfileChoices.SINGLE_1C12P: cable_profiles.Single1C12PCableProfile, + CableProfileChoices.SINGLE_1C16P: cable_profiles.Single1C16PCableProfile, + CableProfileChoices.TRUNK_2C1P: cable_profiles.Trunk2C1PCableProfile, + CableProfileChoices.TRUNK_2C2P: cable_profiles.Trunk2C2PCableProfile, + CableProfileChoices.TRUNK_2C4P: cable_profiles.Trunk2C4PCableProfile, + CableProfileChoices.TRUNK_2C4P_SHUFFLE: cable_profiles.Trunk2C4PShuffleCableProfile, + CableProfileChoices.TRUNK_2C6P: cable_profiles.Trunk2C6PCableProfile, + CableProfileChoices.TRUNK_2C8P: cable_profiles.Trunk2C8PCableProfile, + CableProfileChoices.TRUNK_2C12P: cable_profiles.Trunk2C12PCableProfile, + CableProfileChoices.TRUNK_4C1P: cable_profiles.Trunk4C1PCableProfile, + CableProfileChoices.TRUNK_4C2P: cable_profiles.Trunk4C2PCableProfile, + CableProfileChoices.TRUNK_4C4P: cable_profiles.Trunk4C4PCableProfile, + CableProfileChoices.TRUNK_4C4P_SHUFFLE: cable_profiles.Trunk4C4PShuffleCableProfile, + CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile, + CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile, + CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile, + CableProfileChoices.BREAKOUT_1C4P_4C1P: cable_profiles.Breakout1C4Px4C1PCableProfile, + CableProfileChoices.BREAKOUT_1C6P_6C1P: cable_profiles.Breakout1C6Px6C1PCableProfile, + CableProfileChoices.BREAKOUT_2C4P_8C1P_SHUFFLE: cable_profiles.Breakout2C4Px8C1PShuffleCableProfile, }.get(self.profile) def _get_x_terminations(self, side): @@ -338,14 +359,33 @@ class Cable(PrimaryModel): ct.delete() # Save any new CableTerminations + profile = self.profile_class() if self.profile else None for i, termination in enumerate(self.a_terminations, start=1): if not termination.pk or termination not in a_terminations: - position = i if self.profile and isinstance(termination, PathEndpoint) else None - CableTermination(cable=self, cable_end='A', position=position, termination=termination).save() + connector = positions = None + if profile: + connector = i + positions = profile.get_position_list(profile.a_connectors[i]) + CableTermination( + cable=self, + cable_end=CableEndChoices.SIDE_A, + connector=connector, + positions=positions, + termination=termination + ).save() for i, termination in enumerate(self.b_terminations, start=1): if not termination.pk or termination not in b_terminations: - position = i if self.profile and isinstance(termination, PathEndpoint) else None - CableTermination(cable=self, cable_end='B', position=position, termination=termination).save() + connector = positions = None + if profile: + connector = i + positions = profile.get_position_list(profile.b_connectors[i]) + CableTermination( + cable=self, + cable_end=CableEndChoices.SIDE_B, + connector=connector, + positions=positions, + termination=termination + ).save() class CableTermination(ChangeLoggedModel): @@ -372,13 +412,23 @@ class CableTermination(ChangeLoggedModel): ct_field='termination_type', fk_field='termination_id' ) - position = models.PositiveIntegerField( + connector = models.PositiveSmallIntegerField( blank=True, null=True, validators=( - MinValueValidator(CABLE_POSITION_MIN), - MaxValueValidator(CABLE_POSITION_MAX) - ) + MinValueValidator(CABLE_CONNECTOR_MIN), + MaxValueValidator(CABLE_CONNECTOR_MAX) + ), + ) + positions = ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=( + MinValueValidator(CABLE_POSITION_MIN), + MaxValueValidator(CABLE_POSITION_MAX) + ) + ), + blank=True, + null=True, ) # Cached associations to enable efficient filtering @@ -410,15 +460,15 @@ class CableTermination(ChangeLoggedModel): objects = RestrictedQuerySet.as_manager() class Meta: - ordering = ('cable', 'cable_end', 'position', 'pk') + ordering = ('cable', 'cable_end', 'connector', 'pk') constraints = ( models.UniqueConstraint( fields=('termination_type', 'termination_id'), name='%(app_label)s_%(class)s_unique_termination' ), models.UniqueConstraint( - fields=('cable', 'cable_end', 'position'), - name='%(app_label)s_%(class)s_unique_position' + fields=('cable', 'cable_end', 'connector'), + name='%(app_label)s_%(class)s_unique_connector' ), ) verbose_name = _('cable termination') @@ -481,9 +531,7 @@ class CableTermination(ChangeLoggedModel): # Set the cable on the terminating object termination = self.termination._meta.model.objects.get(pk=self.termination_id) termination.snapshot() - termination.cable = self.cable - termination.cable_end = self.cable_end - termination.cable_position = self.position + termination.set_cable_termination(self) termination.save() def delete(self, *args, **kwargs): @@ -491,9 +539,7 @@ class CableTermination(ChangeLoggedModel): # Delete the cable association on the terminating object termination = self.termination._meta.model.objects.get(pk=self.termination_id) termination.snapshot() - termination.cable = None - termination.cable_end = None - termination.cable_position = None + termination.clear_cable_termination(self) termination.save() super().delete(*args, **kwargs) @@ -701,9 +747,9 @@ class CablePath(models.Model): path.append([ object_to_path_node(t) for t in terminations ]) - # If not null, push cable_position onto the stack - if terminations[0].cable_position is not None: - position_stack.append([terminations[0].cable_position]) + # If not null, push cable position onto the stack + if isinstance(terminations[0], PathEndpoint) and terminations[0].cable_positions: + position_stack.append([terminations[0].cable_positions[0]]) # Step 2: Determine the attached links (Cable or WirelessLink), if any links = list(dict.fromkeys( @@ -744,8 +790,9 @@ class CablePath(models.Model): # Profile-based tracing if links[0].profile: cable_profile = links[0].profile_class() - peer_cable_terminations = cable_profile.get_peer_terminations(terminations, position_stack) - remote_terminations = [ct.termination for ct in peer_cable_terminations] + term, position = cable_profile.get_peer_termination(terminations[0], position_stack.pop()[0]) + remote_terminations = [term] + position_stack.append([position]) # Legacy (positionless) behavior else: diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index e2077e9fe..0b96cc0f7 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1,6 +1,7 @@ from functools import cached_property from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -177,15 +178,24 @@ class CabledObjectModel(models.Model): blank=True, null=True ) - cable_position = models.PositiveIntegerField( - verbose_name=_('cable position'), + cable_connector = models.PositiveSmallIntegerField( blank=True, null=True, validators=( - MinValueValidator(CABLE_POSITION_MIN), - MaxValueValidator(CABLE_POSITION_MAX) + MinValueValidator(CABLE_CONNECTOR_MIN), + MaxValueValidator(CABLE_CONNECTOR_MAX) ), ) + cable_positions = ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=( + MinValueValidator(CABLE_POSITION_MIN), + MaxValueValidator(CABLE_POSITION_MAX) + ) + ), + blank=True, + null=True, + ) mark_connected = models.BooleanField( verbose_name=_('mark connected'), default=False, @@ -210,18 +220,31 @@ class CabledObjectModel(models.Model): raise ValidationError({ "cable_end": _("Must specify cable end (A or B) when attaching a cable.") }) - if self.cable_end and not self.cable: - raise ValidationError({ - "cable_end": _("Cable end must not be set without a cable.") - }) - if self.cable_position and not self.cable: - raise ValidationError({ - "cable_position": _("Cable termination position must not be set without a cable.") - }) - if self.mark_connected and self.cable: - raise ValidationError({ - "mark_connected": _("Cannot mark as connected with a cable attached.") - }) + if self.cable_connector and not self.cable_positions: + raise ValidationError({ + "cable_positions": _("Must specify position(s) when specifying a cable connector.") + }) + if self.cable_positions and not self.cable_connector: + raise ValidationError({ + "cable_positions": _("Cable positions cannot be set without a cable connector.") + }) + if self.mark_connected: + raise ValidationError({ + "mark_connected": _("Cannot mark as connected with a cable attached.") + }) + else: + if self.cable_end: + raise ValidationError({ + "cable_end": _("Cable end must not be set without a cable.") + }) + if self.cable_connector: + raise ValidationError({ + "cable_connector": _("Cable connector must not be set without a cable.") + }) + if self.cable_positions: + raise ValidationError({ + "cable_positions": _("Cable termination positions must not be set without a cable.") + }) @property def link(self): @@ -256,6 +279,22 @@ class CabledObjectModel(models.Model): return None return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B + def set_cable_termination(self, termination): + """Save attributes from the given CableTermination on the terminating object.""" + self.cable = termination.cable + self.cable_end = termination.cable_end + self.cable_connector = termination.connector + self.cable_positions = termination.positions + set_cable_termination.alters_data = True + + def clear_cable_termination(self, termination): + """Clear all cable termination attributes from the terminating object.""" + self.cable = None + self.cable_end = None + self.cable_connector = None + self.cable_positions = None + clear_cable_termination.alters_data = True + class PathEndpoint(models.Model): """ diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index d4783bc3c..3bda18755 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2586,7 +2586,7 @@ class CableTest(APIViewTestCases.APIViewTestCase): 'object_id': interfaces[14].pk, }], 'label': 'Cable 4', - 'profile': CableProfileChoices.STRAIGHT_SINGLE, + 'profile': CableProfileChoices.SINGLE_1C1P, }, { 'a_terminations': [{ @@ -2598,7 +2598,7 @@ class CableTest(APIViewTestCases.APIViewTestCase): 'object_id': interfaces[15].pk, }], 'label': 'Cable 5', - 'profile': CableProfileChoices.STRAIGHT_SINGLE, + 'profile': CableProfileChoices.SINGLE_1C1P, }, { 'a_terminations': [{ @@ -2620,7 +2620,9 @@ class CableTerminationTest( APIViewTestCases.ListObjectsViewTestCase, ): model = CableTermination - brief_fields = ['cable', 'cable_end', 'display', 'id', 'position', 'termination_id', 'termination_type', 'url'] + brief_fields = [ + 'cable', 'cable_end', 'connector', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url', + ] @classmethod def setUpTestData(cls): diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py index 0f9a704c5..27ecf962f 100644 --- a/netbox/dcim/tests/test_cablepaths2.py +++ b/netbox/dcim/tests/test_cablepaths2.py @@ -1,3 +1,5 @@ +from unittest import skip + from circuits.models import CircuitTermination from dcim.choices import CableProfileChoices from dcim.models import * @@ -14,11 +16,11 @@ class CablePathTests(CablePathTestCase): 2XX: Topology tests replicated from the legacy test case and adapted to use profiles """ - def test_101_cable_profile_straight_single(self): + def test_101_cable_profile_single_1c1p(self): """ [IF1] --C1-- [IF2] - Cable profile: Straight single + Cable profile: Single connector, single position """ interfaces = [ Interface.objects.create(device=self.device, name='Interface 1'), @@ -27,7 +29,7 @@ class CablePathTests(CablePathTestCase): # Create cable 1 cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_SINGLE, + profile=CableProfileChoices.SINGLE_1C1P, a_terminations=[interfaces[0]], b_terminations=[interfaces[1]], ) @@ -49,8 +51,10 @@ class CablePathTests(CablePathTestCase): interfaces[1].refresh_from_db() self.assertPathIsSet(interfaces[0], path1) self.assertPathIsSet(interfaces[1], path2) - self.assertEqual(interfaces[0].cable_position, 1) - self.assertEqual(interfaces[1].cable_position, 1) + self.assertEqual(interfaces[0].cable_connector, 1) + self.assertEqual(interfaces[0].cable_positions, [1]) + self.assertEqual(interfaces[1].cable_connector, 1) + self.assertEqual(interfaces[1].cable_positions, [1]) # Test SVG generation CableTraceSVG(interfaces[0]).render() @@ -61,12 +65,148 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_102_cable_profile_straight_multi(self): + def test_102_cable_profile_single_1c2p(self): + """ + [IF1] --C1-- [FP1][RP1] --C3-- [RP2][FP3] --C4-- [IF3] + [IF2] --C2-- [FP2] [FP4] --C5-- [IF4] + + Cable profile: Single connector, multiple positions + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=2), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=2, + ), + ]) + + # Create cables + cable1 = Cable( + a_terminations=[interfaces[0]], + b_terminations=[front_ports[0]], + ) + cable1.clean() + cable1.save() + cable2 = Cable( + a_terminations=[interfaces[1]], + b_terminations=[front_ports[1]], + ) + cable2.clean() + cable2.save() + cable3 = Cable( + profile=CableProfileChoices.SINGLE_1C2P, + a_terminations=[rear_ports[0]], + b_terminations=[rear_ports[1]], + ) + cable3.clean() + cable3.save() + cable4 = Cable( + a_terminations=[interfaces[2]], + b_terminations=[front_ports[2]], + ) + cable4.clean() + cable4.save() + cable5 = Cable( + a_terminations=[interfaces[3]], + b_terminations=[front_ports[3]], + ) + cable5.clean() + cable5.save() + + path1 = self.assertPathExists( + ( + interfaces[0], cable1, front_ports[0], rear_ports[0], cable3, rear_ports[1], front_ports[2], cable4, + interfaces[2], + ), + is_complete=True, + is_active=True + ) + path2 = self.assertPathExists( + ( + interfaces[1], cable2, front_ports[1], rear_ports[0], cable3, rear_ports[1], front_ports[3], cable5, + interfaces[3], + ), + is_complete=True, + is_active=True + ) + path3 = self.assertPathExists( + ( + interfaces[2], cable4, front_ports[2], rear_ports[1], cable3, rear_ports[0], front_ports[0], cable1, + interfaces[0], + ), + is_complete=True, + is_active=True + ) + path4 = self.assertPathExists( + ( + interfaces[3], cable5, front_ports[3], rear_ports[1], cable3, rear_ports[0], front_ports[1], cable2, + interfaces[1], + ), + is_complete=True, + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 4) + for iface in interfaces: + iface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + self.assertPathIsSet(interfaces[3], path4) + for rear_port in rear_ports: + rear_port.refresh_from_db() + self.assertEqual(rear_ports[0].cable_connector, 1) + self.assertEqual(rear_ports[0].cable_positions, [1, 2]) + self.assertEqual(rear_ports[1].cable_connector, 1) + self.assertEqual(rear_ports[1].cable_positions, [1, 2]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_103_cable_profile_trunk_2c1p(self): """ [IF1] --C1-- [IF3] [IF2] [IF4] - Cable profile: Straight multi + Cable profile: Multiple connectors, single position """ interfaces = [ Interface.objects.create(device=self.device, name='Interface 1'), @@ -77,7 +217,7 @@ class CablePathTests(CablePathTestCase): # Create cable 1 cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.TRUNK_2C1P, a_terminations=[interfaces[0], interfaces[1]], b_terminations=[interfaces[2], interfaces[3]], ) @@ -112,10 +252,14 @@ class CablePathTests(CablePathTestCase): self.assertPathIsSet(interfaces[1], path2) self.assertPathIsSet(interfaces[2], path3) self.assertPathIsSet(interfaces[3], path4) - self.assertEqual(interfaces[0].cable_position, 1) - self.assertEqual(interfaces[1].cable_position, 2) - self.assertEqual(interfaces[2].cable_position, 1) - self.assertEqual(interfaces[3].cable_position, 2) + self.assertEqual(interfaces[0].cable_connector, 1) + self.assertEqual(interfaces[0].cable_positions, [1]) + self.assertEqual(interfaces[1].cable_connector, 2) + self.assertEqual(interfaces[1].cable_positions, [1]) + self.assertEqual(interfaces[2].cable_connector, 1) + self.assertEqual(interfaces[2].cable_positions, [1]) + self.assertEqual(interfaces[3].cable_connector, 2) + self.assertEqual(interfaces[3].cable_positions, [1]) # Test SVG generation CableTraceSVG(interfaces[0]).render() @@ -126,16 +270,378 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) - def test_103_cable_profile_2x2_mpo8(self): + def test_104_cable_profile_trunk_2c2p(self): """ - [IF1:1] --C1-- [IF3:1] - [IF1:2] [IF3:2] - [IF1:3] [IF3:3] - [IF1:4] [IF3:4] - [IF2:1] [IF4:1] - [IF2:2] [IF4:2] - [IF2:3] [IF4:3] - [IF2:4] [IF4:4] + [IF1] --C1-- [FP1][RP1] --C9-- [RP3][FP5] --C5-- [IF5] + [IF2] --C2-- [FP2] [FP6] --C6-- [IF6] + [IF3] --C3-- [FP3][RP2] [RP4][FP7] --C7-- [IF7] + [IF4] --C4-- [FP4] [FP8] --C8-- [IF8] + + Cable profile: Multiple connectors, multiple positions + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + Interface.objects.create(device=self.device, name='Interface 5'), + Interface.objects.create(device=self.device, name='Interface 6'), + Interface.objects.create(device=self.device, name='Interface 7'), + Interface.objects.create(device=self.device, name='Interface 8'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 3', positions=2), + RearPort.objects.create(device=self.device, name='Rear Port 4', positions=2), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + FrontPort.objects.create(device=self.device, name='Front Port 5'), + FrontPort.objects.create(device=self.device, name='Front Port 6'), + FrontPort.objects.create(device=self.device, name='Front Port 7'), + FrontPort.objects.create(device=self.device, name='Front Port 8'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[4], + front_port_position=1, + rear_port=rear_ports[2], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[5], + front_port_position=1, + rear_port=rear_ports[2], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[6], + front_port_position=1, + rear_port=rear_ports[3], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[7], + front_port_position=1, + rear_port=rear_ports[3], + rear_port_position=2, + ), + ]) + + # Create cables + cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[front_ports[0]]) + cable1.clean() + cable1.save() + cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[front_ports[1]]) + cable2.clean() + cable2.save() + cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[front_ports[2]]) + cable3.clean() + cable3.save() + cable4 = Cable(a_terminations=[interfaces[3]], b_terminations=[front_ports[3]]) + cable4.clean() + cable4.save() + cable5 = Cable(a_terminations=[interfaces[4]], b_terminations=[front_ports[4]]) + cable5.clean() + cable5.save() + cable6 = Cable(a_terminations=[interfaces[5]], b_terminations=[front_ports[5]]) + cable6.clean() + cable6.save() + cable7 = Cable(a_terminations=[interfaces[6]], b_terminations=[front_ports[6]]) + cable7.clean() + cable7.save() + cable8 = Cable(a_terminations=[interfaces[7]], b_terminations=[front_ports[7]]) + cable8.clean() + cable8.save() + cable9 = Cable( + profile=CableProfileChoices.TRUNK_2C2P, + a_terminations=[rear_ports[0], rear_ports[1]], + b_terminations=[rear_ports[2], rear_ports[3]] + ) + cable9.clean() + cable9.save() + + path1 = self.assertPathExists( + ( + interfaces[0], cable1, front_ports[0], rear_ports[0], cable9, rear_ports[2], front_ports[4], cable5, + interfaces[4], + ), + is_complete=True, + is_active=True + ) + path2 = self.assertPathExists( + ( + interfaces[1], cable2, front_ports[1], rear_ports[0], cable9, rear_ports[2], front_ports[5], cable6, + interfaces[5], + ), + is_complete=True, + is_active=True + ) + path3 = self.assertPathExists( + ( + interfaces[2], cable3, front_ports[2], rear_ports[1], cable9, rear_ports[3], front_ports[6], cable7, + interfaces[6], + ), + is_complete=True, + is_active=True + ) + path4 = self.assertPathExists( + ( + interfaces[3], cable4, front_ports[3], rear_ports[1], cable9, rear_ports[3], front_ports[7], cable8, + interfaces[7], + ), + is_complete=True, + is_active=True + ) + path5 = self.assertPathExists( + ( + interfaces[4], cable5, front_ports[4], rear_ports[2], cable9, rear_ports[0], front_ports[0], cable1, + interfaces[0], + ), + is_complete=True, + is_active=True + ) + path6 = self.assertPathExists( + ( + interfaces[5], cable6, front_ports[5], rear_ports[2], cable9, rear_ports[0], front_ports[1], cable2, + interfaces[1], + ), + is_complete=True, + is_active=True + ) + path7 = self.assertPathExists( + ( + interfaces[6], cable7, front_ports[6], rear_ports[3], cable9, rear_ports[1], front_ports[2], cable3, + interfaces[2], + ), + is_complete=True, + is_active=True + ) + path8 = self.assertPathExists( + ( + interfaces[7], cable8, front_ports[7], rear_ports[3], cable9, rear_ports[1], front_ports[3], cable4, + interfaces[3], + ), + is_complete=True, + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 8) + for iface in interfaces: + iface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + self.assertPathIsSet(interfaces[3], path4) + self.assertPathIsSet(interfaces[4], path5) + self.assertPathIsSet(interfaces[5], path6) + self.assertPathIsSet(interfaces[6], path7) + self.assertPathIsSet(interfaces[7], path8) + for rear_port in rear_ports: + rear_port.refresh_from_db() + self.assertEqual(rear_ports[0].cable_connector, 1) + self.assertEqual(rear_ports[0].cable_positions, [1, 2]) + self.assertEqual(rear_ports[1].cable_connector, 2) + self.assertEqual(rear_ports[1].cable_positions, [1, 2]) + self.assertEqual(rear_ports[2].cable_connector, 1) + self.assertEqual(rear_ports[2].cable_positions, [1, 2]) + self.assertEqual(rear_ports[3].cable_connector, 2) + self.assertEqual(rear_ports[3].cable_positions, [1, 2]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_105_cable_profile_breakout(self): + """ + [IF1] --C1-- [FP1][RP1] --C2-- [IF5] + [IF2] --C3-- [FP2] [IF6] + [IF3] --C4-- [FP3] [IF7] + [IF4] --C5-- [FP4] [IF8] + + Cable profile: 1:4 breakout + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + Interface.objects.create(device=self.device, name='Interface 5'), + Interface.objects.create(device=self.device, name='Interface 6'), + Interface.objects.create(device=self.device, name='Interface 7'), + Interface.objects.create(device=self.device, name='Interface 8'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=3, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=4, + ), + ]) + + # Create cables + cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[front_ports[0]]) + cable1.clean() + cable1.save() + cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[front_ports[1]]) + cable2.clean() + cable2.save() + cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[front_ports[2]]) + cable3.clean() + cable3.save() + cable4 = Cable(a_terminations=[interfaces[3]], b_terminations=[front_ports[3]]) + cable4.clean() + cable4.save() + cable5 = Cable( + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[rear_ports[0]], + b_terminations=interfaces[4:8], + ) + cable5.clean() + cable5.save() + + path1 = self.assertPathExists( + (interfaces[0], cable1, front_ports[0], rear_ports[0], cable5, interfaces[4]), + is_complete=True, + is_active=True + ) + path2 = self.assertPathExists( + (interfaces[1], cable2, front_ports[1], rear_ports[0], cable5, interfaces[5]), + is_complete=True, + is_active=True + ) + path3 = self.assertPathExists( + (interfaces[2], cable3, front_ports[2], rear_ports[0], cable5, interfaces[6]), + is_complete=True, + is_active=True + ) + path4 = self.assertPathExists( + (interfaces[3], cable4, front_ports[3], rear_ports[0], cable5, interfaces[7]), + is_complete=True, + is_active=True + ) + path5 = self.assertPathExists( + (interfaces[4], cable5, rear_ports[0], front_ports[0], cable1, interfaces[0]), + is_complete=True, + is_active=True + ) + path6 = self.assertPathExists( + (interfaces[5], cable5, rear_ports[0], front_ports[1], cable2, interfaces[1]), + is_complete=True, + is_active=True + ) + path7 = self.assertPathExists( + (interfaces[6], cable5, rear_ports[0], front_ports[2], cable3, interfaces[2]), + is_complete=True, + is_active=True + ) + path8 = self.assertPathExists( + (interfaces[7], cable5, rear_ports[0], front_ports[3], cable4, interfaces[3]), + is_complete=True, + is_active=True + ) + self.assertEqual(CablePath.objects.count(), 8) + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + self.assertPathIsSet(interfaces[3], path4) + self.assertPathIsSet(interfaces[4], path5) + self.assertPathIsSet(interfaces[5], path6) + self.assertPathIsSet(interfaces[6], path7) + self.assertPathIsSet(interfaces[7], path8) + self.assertEqual(interfaces[4].cable_connector, 1) + self.assertEqual(interfaces[4].cable_positions, [1]) + self.assertEqual(interfaces[5].cable_connector, 2) + self.assertEqual(interfaces[5].cable_positions, [1]) + self.assertEqual(interfaces[6].cable_connector, 3) + self.assertEqual(interfaces[6].cable_positions, [1]) + self.assertEqual(interfaces[7].cable_connector, 4) + self.assertEqual(interfaces[7].cable_positions, [1]) + rear_ports[0].refresh_from_db() + self.assertEqual(rear_ports[0].cable_connector, 1) + self.assertEqual(rear_ports[0].cable_positions, [1, 2, 3, 4]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_106_cable_profile_shuffle(self): + """ + [IF1] --C1-- [FP1][RP1] --C17-- [RP3][FP9] --C9-- [IF9] + [IF2] --C2-- [FP2] [FP10] --C10-- [IF10] + [IF3] --C3-- [FP3] [FP11] --C11-- [IF11] + [IF4] --C4-- [FP4] [FP12] --C12-- [IF12] + [IF5] --C5-- [FP5][RP2] [RP4][FP13] --C13-- [IF9] + [IF6] --C6-- [FP6] [FP14] --C14-- [IF10] + [IF7] --C7-- [FP7] [FP15] --C15-- [IF11] + [IF8] --C8-- [FP8] [FP16] --C16-- [IF12] Cable profile: Shuffle (2x2 MPO8) """ @@ -159,197 +665,138 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 4:3'), Interface.objects.create(device=self.device, name='Interface 4:4'), ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4), + RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4), + RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), + FrontPort.objects.create(device=self.device, name='Front Port 5'), + FrontPort.objects.create(device=self.device, name='Front Port 6'), + FrontPort.objects.create(device=self.device, name='Front Port 7'), + FrontPort.objects.create(device=self.device, name='Front Port 8'), + FrontPort.objects.create(device=self.device, name='Front Port 9'), + FrontPort.objects.create(device=self.device, name='Front Port 10'), + FrontPort.objects.create(device=self.device, name='Front Port 11'), + FrontPort.objects.create(device=self.device, name='Front Port 12'), + FrontPort.objects.create(device=self.device, name='Front Port 13'), + FrontPort.objects.create(device=self.device, name='Front Port 14'), + FrontPort.objects.create(device=self.device, name='Front Port 15'), + FrontPort.objects.create(device=self.device, name='Front Port 16'), + ] + port_mappings = [] + for i, front_port in enumerate(front_ports): + port_mappings.append( + PortMapping( + device=self.device, + front_port=front_ports[i], + front_port_position=1, + rear_port=rear_ports[int(i / 4)], + rear_port_position=(i % 4) + 1, + ), + ) + PortMapping.objects.bulk_create(port_mappings) - # Create cable 1 - cable1 = Cable( - profile=CableProfileChoices.SHUFFLE_2X2_MPO8, - a_terminations=interfaces[0:8], - b_terminations=interfaces[8:16], + # Create cables + cables = [] + for interface, front_port in zip(interfaces, front_ports): + cable = Cable(a_terminations=[interface], b_terminations=[front_port]) + cable.clean() + cable.save() + cables.append(cable) + shuffle_cable = Cable( + profile=CableProfileChoices.TRUNK_2C4P_SHUFFLE, + a_terminations=rear_ports[0:2], + b_terminations=rear_ports[2:4], ) - cable1.clean() - cable1.save() + shuffle_cable.clean() + shuffle_cable.save() paths = [ # A-to-B paths self.assertPathExists( - (interfaces[0], cable1, interfaces[8]), is_complete=True, is_active=True + ( + interfaces[0], cables[0], front_ports[0], rear_ports[0], shuffle_cable, rear_ports[2], + front_ports[8], cables[8], interfaces[8], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[1], cable1, interfaces[9]), is_complete=True, is_active=True + ( + interfaces[1], cables[1], front_ports[1], rear_ports[0], shuffle_cable, rear_ports[2], + front_ports[9], cables[9], interfaces[9], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[2], cable1, interfaces[12]), is_complete=True, is_active=True + ( + interfaces[2], cables[2], front_ports[2], rear_ports[0], shuffle_cable, rear_ports[3], + front_ports[12], cables[12], interfaces[12], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[3], cable1, interfaces[13]), is_complete=True, is_active=True + ( + interfaces[3], cables[3], front_ports[3], rear_ports[0], shuffle_cable, rear_ports[3], + front_ports[13], cables[13], interfaces[13], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[4], cable1, interfaces[10]), is_complete=True, is_active=True + ( + interfaces[4], cables[4], front_ports[4], rear_ports[1], shuffle_cable, rear_ports[2], + front_ports[10], cables[10], interfaces[10], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[5], cable1, interfaces[11]), is_complete=True, is_active=True + ( + interfaces[5], cables[5], front_ports[5], rear_ports[1], shuffle_cable, rear_ports[2], + front_ports[11], cables[11], interfaces[11], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[6], cable1, interfaces[14]), is_complete=True, is_active=True + ( + interfaces[6], cables[6], front_ports[6], rear_ports[1], shuffle_cable, rear_ports[3], + front_ports[14], cables[14], interfaces[14], + ), + is_complete=True, + is_active=True ), self.assertPathExists( - (interfaces[7], cable1, interfaces[15]), is_complete=True, is_active=True - ), - # B-to-A paths - self.assertPathExists( - (interfaces[8], cable1, interfaces[0]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[9], cable1, interfaces[1]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[10], cable1, interfaces[4]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[11], cable1, interfaces[5]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[12], cable1, interfaces[2]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[13], cable1, interfaces[3]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[14], cable1, interfaces[6]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[15], cable1, interfaces[7]), is_complete=True, is_active=True + ( + interfaces[7], cables[7], front_ports[7], rear_ports[1], shuffle_cable, rear_ports[3], + front_ports[15], cables[15], interfaces[15], + ), + is_complete=True, + is_active=True ), ] - self.assertEqual(CablePath.objects.count(), len(paths)) + self.assertEqual(CablePath.objects.count(), len(paths) * 2) for i, (interface, path) in enumerate(zip(interfaces, paths)): interface.refresh_from_db() self.assertPathIsSet(interface, path) - self.assertEqual(interface.cable_end, 'A' if i < 8 else 'B') - self.assertEqual(interface.cable_position, (i % 8) + 1) + for i, rear_port in enumerate(rear_ports): + rear_port.refresh_from_db() + self.assertEqual(rear_port.cable_connector, (i % 2) + 1) + self.assertEqual(rear_port.cable_positions, [1, 2, 3, 4]) # Test SVG generation CableTraceSVG(interfaces[0]).render() - # Delete cable 1 - cable1.delete() - - # Check that all CablePaths have been deleted - self.assertEqual(CablePath.objects.count(), 0) - - def test_104_cable_profile_4x4_mpo8(self): - """ - [IF1:1] --C1-- [IF3:1] - [IF1:2] [IF3:2] - [IF1:3] [IF3:3] - [IF1:4] [IF3:4] - [IF2:1] [IF4:1] - [IF2:2] [IF4:2] - [IF2:3] [IF4:3] - [IF2:4] [IF4:4] - - Cable profile: Shuffle (4x4 MPO8) - """ - interfaces = [ - # A side - Interface.objects.create(device=self.device, name='Interface 1:1'), - Interface.objects.create(device=self.device, name='Interface 1:2'), - Interface.objects.create(device=self.device, name='Interface 2:1'), - Interface.objects.create(device=self.device, name='Interface 2:2'), - Interface.objects.create(device=self.device, name='Interface 3:1'), - Interface.objects.create(device=self.device, name='Interface 3:2'), - Interface.objects.create(device=self.device, name='Interface 4:1'), - Interface.objects.create(device=self.device, name='Interface 4:2'), - # B side - Interface.objects.create(device=self.device, name='Interface 5:1'), - Interface.objects.create(device=self.device, name='Interface 5:2'), - Interface.objects.create(device=self.device, name='Interface 6:1'), - Interface.objects.create(device=self.device, name='Interface 6:2'), - Interface.objects.create(device=self.device, name='Interface 7:1'), - Interface.objects.create(device=self.device, name='Interface 7:2'), - Interface.objects.create(device=self.device, name='Interface 8:1'), - Interface.objects.create(device=self.device, name='Interface 8:2'), - ] - - # Create cable 1 - cable1 = Cable( - profile=CableProfileChoices.SHUFFLE_4X4_MPO8, - a_terminations=interfaces[0:8], - b_terminations=interfaces[8:16], - ) - cable1.clean() - cable1.save() - - paths = [ - # A-to-B paths - self.assertPathExists( - (interfaces[0], cable1, interfaces[8]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[1], cable1, interfaces[10]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[2], cable1, interfaces[12]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[3], cable1, interfaces[14]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[4], cable1, interfaces[9]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[5], cable1, interfaces[11]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[6], cable1, interfaces[13]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[7], cable1, interfaces[15]), is_complete=True, is_active=True - ), - # B-to-A paths - self.assertPathExists( - (interfaces[8], cable1, interfaces[0]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[9], cable1, interfaces[4]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[10], cable1, interfaces[1]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[11], cable1, interfaces[5]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[12], cable1, interfaces[2]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[13], cable1, interfaces[6]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[14], cable1, interfaces[3]), is_complete=True, is_active=True - ), - self.assertPathExists( - (interfaces[15], cable1, interfaces[7]), is_complete=True, is_active=True - ), - ] - self.assertEqual(CablePath.objects.count(), len(paths)) - - for i, (interface, path) in enumerate(zip(interfaces, paths)): - interface.refresh_from_db() - self.assertPathIsSet(interface, path) - self.assertEqual(interface.cable_end, 'A' if i < 8 else 'B') - self.assertEqual(interface.cable_position, (i % 8) + 1) - - # Test SVG generation - CableTraceSVG(interfaces[0]).render() - - # Delete cable 1 - cable1.delete() - - # Check that all CablePaths have been deleted - self.assertEqual(CablePath.objects.count(), 0) - def test_202_single_path_via_pass_through_with_breakouts(self): """ [IF1] --C1-- [FP1] [RP1] --C2-- [IF3] @@ -361,8 +808,8 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 3'), Interface.objects.create(device=self.device, name='Interface 4'), ] - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') - frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1', positions=4) PortMapping.objects.bulk_create([ PortMapping( device=self.device, @@ -371,18 +818,39 @@ class CablePathTests(CablePathTestCase): rear_port=rearport1, rear_port_position=1, ), + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=2, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=3, + rear_port=rearport1, + rear_port_position=3, + ), + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=4, + rear_port=rearport1, + rear_port_position=4, + ), ]) # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, - a_terminations=[interfaces[0], interfaces[1]], - b_terminations=[frontport1], + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[frontport1], + b_terminations=[interfaces[0], interfaces[1]], ) cable1.clean() cable1.save() cable2 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, a_terminations=[rearport1], b_terminations=[interfaces[2], interfaces[3]] ) @@ -424,10 +892,10 @@ class CablePathTests(CablePathTestCase): def test_204_multiple_paths_via_pass_through_with_breakouts(self): """ - [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF4] - [IF2] [IF5] - [IF3] --C2-- [FP1:2] [FP2:2] --C5-- [IF6] - [IF4] [IF7] + [IF1] --C1-- [FP1] [RP1] --C3-- [RP2] [FP3] --C4-- [IF5] + [IF2] [IF6] + [IF3] --C2-- [FP2] [FP4] --C5-- [IF7] + [IF4] [IF8] """ interfaces = [ Interface.objects.create(device=self.device, name='Interface 1'), @@ -439,76 +907,104 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 7'), Interface.objects.create(device=self.device, name='Interface 8'), ] - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') - frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') - frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') - frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=8) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=8) + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1', positions=4) + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2', positions=4) + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 2:1', positions=4) + frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 2:2', positions=4) PortMapping.objects.bulk_create([ PortMapping( device=self.device, - front_port=frontport1_1, + front_port=frontport1, front_port_position=1, rear_port=rearport1, rear_port_position=1, ), PortMapping( device=self.device, - front_port=frontport1_2, - front_port_position=1, + front_port=frontport1, + front_port_position=2, rear_port=rearport1, rear_port_position=2, ), PortMapping( device=self.device, - front_port=frontport2_1, + front_port=frontport2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=5, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=2, + rear_port=rearport1, + rear_port_position=6, + ), + PortMapping( + device=self.device, + front_port=frontport3, front_port_position=1, rear_port=rearport2, rear_port_position=1, ), PortMapping( device=self.device, - front_port=frontport2_2, - front_port_position=1, + front_port=frontport3, + front_port_position=2, rear_port=rearport2, rear_port_position=2, ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=1, + rear_port=rearport2, + rear_port_position=5, + ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=2, + rear_port=rearport2, + rear_port_position=6, + ), ]) # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, - a_terminations=[interfaces[0], interfaces[1]], - b_terminations=[frontport1_1] + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[frontport1], + b_terminations=[interfaces[0], interfaces[1]], ) cable1.clean() cable1.save() cable2 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, - a_terminations=[interfaces[2], interfaces[3]], - b_terminations=[frontport1_2] + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[frontport2], + b_terminations=[interfaces[2], interfaces[3]], ) cable2.clean() cable2.save() cable3 = Cable( - profile=CableProfileChoices.STRAIGHT_SINGLE, + profile=CableProfileChoices.SINGLE_1C8P, a_terminations=[rearport1], b_terminations=[rearport2] ) cable3.clean() cable3.save() cable4 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, - a_terminations=[frontport2_1], - b_terminations=[interfaces[4], interfaces[5]] + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[frontport3], + b_terminations=[interfaces[4], interfaces[5]], ) cable4.clean() cable4.save() cable5 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, - a_terminations=[frontport2_2], - b_terminations=[interfaces[6], interfaces[7]] + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[frontport4], + b_terminations=[interfaces[6], interfaces[7]], ) cable5.clean() cable5.save() @@ -516,7 +1012,7 @@ class CablePathTests(CablePathTestCase): paths = [ self.assertPathExists( ( - interfaces[0], cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4, + interfaces[0], cable1, frontport1, rearport1, cable3, rearport2, frontport3, cable4, interfaces[4], ), is_complete=True, @@ -524,7 +1020,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[1], cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4, + interfaces[1], cable1, frontport1, rearport1, cable3, rearport2, frontport3, cable4, interfaces[5], ), is_complete=True, @@ -532,7 +1028,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[2], cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5, + interfaces[2], cable2, frontport2, rearport1, cable3, rearport2, frontport4, cable5, interfaces[6], ), is_complete=True, @@ -540,7 +1036,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[3], cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5, + interfaces[3], cable2, frontport2, rearport1, cable3, rearport2, frontport4, cable5, interfaces[7], ), is_complete=True, @@ -548,7 +1044,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[4], cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1, + interfaces[4], cable4, frontport3, rearport2, cable3, rearport1, frontport1, cable1, interfaces[0], ), is_complete=True, @@ -556,7 +1052,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[5], cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1, + interfaces[5], cable4, frontport3, rearport2, cable3, rearport1, frontport1, cable1, interfaces[1], ), is_complete=True, @@ -564,7 +1060,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[6], cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2, + interfaces[6], cable5, frontport4, rearport2, cable3, rearport1, frontport2, cable2, interfaces[2], ), is_complete=True, @@ -572,7 +1068,7 @@ class CablePathTests(CablePathTestCase): ), self.assertPathExists( ( - interfaces[7], cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2, + interfaces[7], cable5, frontport4, rearport2, cable3, rearport1, frontport2, cable2, interfaces[3], ), is_complete=True, @@ -619,14 +1115,14 @@ class CablePathTests(CablePathTestCase): # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, - a_terminations=[interfaces[0], interfaces[1]], - b_terminations=[circuittermination1] + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, + a_terminations=[circuittermination1], + b_terminations=[interfaces[0], interfaces[1]], ) cable1.clean() cable1.save() cable2 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, a_terminations=[circuittermination2], b_terminations=[interfaces[2], interfaces[3]] ) @@ -668,6 +1164,8 @@ class CablePathTests(CablePathTestCase): # Test SVG generation CableTraceSVG(interfaces[0]).render() + # TBD: Is this a topology we want to support? + @skip("Test applicability TBD") def test_217_interface_to_interface_via_rear_ports(self): """ [IF1] --C1-- [FP1] [RP1] --C2-- [RP3] [FP3] --C3-- [IF2] @@ -722,7 +1220,7 @@ class CablePathTests(CablePathTestCase): # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.SINGLE_2C1P, a_terminations=[interfaces[0]], b_terminations=[front_ports[0], front_ports[1]] ) @@ -735,7 +1233,7 @@ class CablePathTests(CablePathTestCase): cable2.clean() cable2.save() cable3 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.SINGLE_2C1P, a_terminations=[interfaces[1]], b_terminations=[front_ports[2], front_ports[3]] ) @@ -803,14 +1301,14 @@ class CablePathTests(CablePathTestCase): # Create cables cable1 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.TRUNK_2C2P, a_terminations=[interfaces[0], interfaces[1]], b_terminations=[frontport1, frontport2] ) cable1.clean() cable1.save() cable2 = Cable( - profile=CableProfileChoices.STRAIGHT_MULTI, + profile=CableProfileChoices.TRUNK_2C2P, a_terminations=[rearport1, rearport2], b_terminations=[interfaces[2], interfaces[3]] ) @@ -819,22 +1317,22 @@ class CablePathTests(CablePathTestCase): # Validate paths self.assertPathExists( - (interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]), + (interfaces[0], cable1, frontport1, rearport1, cable2, interfaces[2]), is_complete=True, is_active=True ) self.assertPathExists( - (interfaces[1], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[3]), + (interfaces[1], cable1, frontport2, rearport2, cable2, interfaces[3]), is_complete=True, is_active=True ) self.assertPathExists( - (interfaces[2], cable2, [rearport1, rearport2], [frontport1, frontport2], cable1, interfaces[0]), + (interfaces[2], cable2, rearport1, frontport1, cable1, interfaces[0]), is_complete=True, is_active=True ) self.assertPathExists( - (interfaces[3], cable2, [rearport1, rearport2], [frontport1, frontport2], cable1, interfaces[1]), + (interfaces[3], cable2, rearport2, frontport2, cable1, interfaces[1]), is_complete=True, is_active=True ) 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): diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index ce4a8c8d5..4b9d0fb5c 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -41,12 +41,12 @@ def create_cablepaths(objects): """ from dcim.models import CablePath - # Arrange objects by cable position. All objects with a null position are grouped together. + # Arrange objects by cable connector. All objects with a null connector are grouped together. origins = defaultdict(list) for obj in objects: - origins[obj.cable_position].append(obj) + origins[obj.cable_connector].append(obj) - for position, objects in origins.items(): + for connector, objects in origins.items(): if cp := CablePath.from_origin(objects): cp.save()