Compare commits

..

7 Commits

Author SHA1 Message Date
Jeremy Stretch
0f08ded8fa Merge ad8f3315bc into 3140060f21 2025-12-14 12:31:59 -05:00
Jeremy Stretch
ad8f3315bc Flesh out cable profiles & tests
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
2025-12-14 12:29:13 -05:00
Jeremy Stretch
5b23866dcb Clean up validation logic 2025-12-12 11:48:51 -05:00
Jeremy Stretch
fa2a70b098 Rename migrations 2025-12-12 11:48:22 -05:00
Jeremy Stretch
60e38ec016 Fix shuffle breakout mapping 2025-12-12 10:53:10 -05:00
Jeremy Stretch
0ac52f3ce4 Cleanup; updated tests
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
2025-12-12 10:26:01 -05:00
Jeremy Stretch
aaa05fe071 #20788: Map positions by connector (WIP) 2025-12-11 16:33:30 -05:00
6 changed files with 111 additions and 146 deletions

View File

@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('circuits', '0054_cable_connector_positions'), ('circuits', '0054_cable_position'),
] ]
operations = [ operations = [

View File

@@ -1,24 +1,17 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import CableEndChoices
from dcim.models import CableTermination from dcim.models import CableTermination
class BaseCableProfile: class BaseCableProfile:
"""Base class for representing a cable profile.""" """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 # Mappings of connectors to their available positions at either end of the cable. For example, a 12-strand MPO
# 12-strand MPO fiber cable would have one connector at either end with six positions (six bidirectional fiber # fiber cable would have one connector at either end with six positions (six bidirectional fiber pairs).
# pairs).
a_connectors = {} a_connectors = {}
b_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): def clean(self, cable):
# Enforce maximum terminations limits # Enforce maximum terminations limits
a_terminations_count = len(cable.a_terminations) a_terminations_count = len(cable.a_terminations)
@@ -51,8 +44,6 @@ class BaseCableProfile:
Return the mapped far-end connector & position for a given cable end the local connector & position. Return the mapped far-end connector & position for a given cable end the local connector & position.
""" """
# By default, assume all positions are symmetrical. # By default, assume all positions are symmetrical.
if self._mapping:
return self._mapping.get((connector, position))
return connector, position return connector, position
def get_peer_termination(self, termination, position): def get_peer_termination(self, termination, position):
@@ -76,182 +67,177 @@ class BaseCableProfile:
return None, None return None, None
# 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 Single1C1PCableProfile(BaseCableProfile): class Single1C1PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 1, 1: [1],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Single1C2PCableProfile(BaseCableProfile): class Single1C2PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 2, 1: [1, 2],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Single1C4PCableProfile(BaseCableProfile): class Single1C4PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Single1C6PCableProfile(BaseCableProfile): class Single1C6PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 6, 1: [1, 2, 3, 4, 5, 6],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Single1C8PCableProfile(BaseCableProfile): class Single1C8PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 8, 1: [1, 2, 3, 4, 5, 6, 7, 8],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Single1C12PCableProfile(BaseCableProfile): class Single1C12PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 12, 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Single1C16PCableProfile(BaseCableProfile): class Single1C16PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 16, 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk2C1PCableProfile(BaseCableProfile): class Trunk2C1PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 1, 1: [1],
2: 1, 2: [1],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk2C2PCableProfile(BaseCableProfile): class Trunk2C2PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 2, 1: [1, 2],
2: 2, 2: [1, 2],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk2C4PCableProfile(BaseCableProfile): class Trunk2C4PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
2: 4, 2: [1, 2, 3, 4],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk2C6PCableProfile(BaseCableProfile): class Trunk2C6PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 6, 1: [1, 2, 3, 4, 5, 6],
2: 6, 2: [1, 2, 3, 4, 5, 6],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk2C8PCableProfile(BaseCableProfile): class Trunk2C8PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 8, 1: [1, 2, 3, 4, 5, 6, 7, 8],
2: 8, 2: [1, 2, 3, 4, 5, 6, 7, 8],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk2C12PCableProfile(BaseCableProfile): class Trunk2C12PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 12, 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
2: 12, 2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk4C1PCableProfile(BaseCableProfile): class Trunk4C1PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 1, 1: [1],
2: 1, 2: [1],
3: 1, 3: [1],
4: 1, 4: [1],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk4C2PCableProfile(BaseCableProfile): class Trunk4C2PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 2, 1: [1, 2],
2: 2, 2: [1, 2],
3: 2, 3: [1, 2],
4: 2, 4: [1, 2],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk4C4PCableProfile(BaseCableProfile): class Trunk4C4PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
2: 4, 2: [1, 2, 3, 4],
3: 4, 3: [1, 2, 3, 4],
4: 4, 4: [1, 2, 3, 4],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk4C6PCableProfile(BaseCableProfile): class Trunk4C6PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 6, 1: [1, 2, 3, 4, 5, 6],
2: 6, 2: [1, 2, 3, 4, 5, 6],
3: 6, 3: [1, 2, 3, 4, 5, 6],
4: 6, 4: [1, 2, 3, 4, 5, 6],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk4C8PCableProfile(BaseCableProfile): class Trunk4C8PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 8, 1: [1, 2, 3, 4, 5, 6, 7, 8],
2: 8, 2: [1, 2, 3, 4, 5, 6, 7, 8],
3: 8, 3: [1, 2, 3, 4, 5, 6, 7, 8],
4: 8, 4: [1, 2, 3, 4, 5, 6, 7, 8],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Trunk8C4PCableProfile(BaseCableProfile): class Trunk8C4PCableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
2: 4, 2: [1, 2, 3, 4],
3: 4, 3: [1, 2, 3, 4],
4: 4, 4: [1, 2, 3, 4],
5: 4, 5: [1, 2, 3, 4],
6: 4, 6: [1, 2, 3, 4],
7: 4, 7: [1, 2, 3, 4],
8: 4, 8: [1, 2, 3, 4],
} }
b_connectors = a_connectors b_connectors = a_connectors
class Breakout1C4Px4C1PCableProfile(BaseCableProfile): class Breakout1x4CableProfile(BaseCableProfile):
"""Breakout 1:4 to 4:1"""
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
} }
b_connectors = { b_connectors = {
1: 1, 1: [1],
2: 1, 2: [1],
3: 1, 3: [1],
4: 1, 4: [1],
} }
_mapping = { _mapping = {
(1, 1): (1, 1), (1, 1): (1, 1),
@@ -263,38 +249,14 @@ class Breakout1C4Px4C1PCableProfile(BaseCableProfile):
(4, 1): (1, 4), (4, 1): (1, 4),
} }
def get_mapped_position(self, side, connector, position):
return self._mapping.get((connector, position))
class Breakout1C6Px6C1PCableProfile(BaseCableProfile):
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 6, 1: [1, 2, 3, 4],
} 2: [1, 2, 3, 4],
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 Shuffle2C4PCableProfile(BaseCableProfile):
a_connectors = {
1: 4,
2: 4,
} }
b_connectors = a_connectors b_connectors = a_connectors
_mapping = { _mapping = {
@@ -308,13 +270,16 @@ class Shuffle2C4PCableProfile(BaseCableProfile):
(2, 4): (2, 4), (2, 4): (2, 4),
} }
def get_mapped_position(self, side, connector, position):
return self._mapping.get((connector, position))
class Shuffle4C4PCableProfile(BaseCableProfile):
class Shuffle4x4MPO8CableProfile(BaseCableProfile):
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
2: 4, 2: [1, 2, 3, 4],
3: 4, 3: [1, 2, 3, 4],
4: 4, 4: [1, 2, 3, 4],
} }
b_connectors = a_connectors b_connectors = a_connectors
_mapping = { _mapping = {
@@ -336,24 +301,27 @@ class Shuffle4C4PCableProfile(BaseCableProfile):
(4, 4): (4, 4), (4, 4): (4, 4),
} }
def get_mapped_position(self, side, connector, position):
return self._mapping.get((connector, position))
class ShuffleBreakout2x8CableProfile(BaseCableProfile): class ShuffleBreakout2x8CableProfile(BaseCableProfile):
""" """
Temporary solution for mapping 2 front/rear ports to 8 discrete interfaces Temporary solution for mapping 2 front/rear ports to 8 discrete interfaces
""" """
a_connectors = { a_connectors = {
1: 4, 1: [1, 2, 3, 4],
2: 4, 2: [1, 2, 3, 4],
} }
b_connectors = { b_connectors = {
1: 1, 1: [1],
2: 1, 2: [1],
3: 1, 3: [1],
4: 1, 4: [1],
5: 1, 5: [1],
6: 1, 6: [1],
7: 1, 7: [1],
8: 1, 8: [1],
} }
_a_mapping = { _a_mapping = {
(1, 1): (1, 1), (1, 1): (1, 1),
@@ -377,6 +345,6 @@ class ShuffleBreakout2x8CableProfile(BaseCableProfile):
} }
def get_mapped_position(self, side, connector, position): def get_mapped_position(self, side, connector, position):
if side.lower() == CableEndChoices.SIDE_A: if side.lower() == 'a':
return self._a_mapping.get((connector, position)) return self._a_mapping.get((connector, position))
return self._b_mapping.get((connector, position)) return self._b_mapping.get((connector, position))

View File

@@ -1744,12 +1744,11 @@ class CableProfileChoices(ChoiceSet):
TRUNK_4C8P = 'trunk-4c8p' TRUNK_4C8P = 'trunk-4c8p'
TRUNK_8C4P = 'trunk-8c4p' TRUNK_8C4P = 'trunk-8c4p'
# Breakouts # Breakouts
BREAKOUT_1C4P_4C1P = 'breakout-1c4p-4c1p' BREAKOUT_1X4 = 'breakout-1x4'
BREAKOUT_1C6P_6C1P = 'breakout-1c6p-6c1p'
SHUFFLE_BREAKOUT_2X8 = 'shuffle-breakout-2x8' SHUFFLE_BREAKOUT_2X8 = 'shuffle-breakout-2x8'
# Shuffles # Shuffles
SHUFFLE_2C4P = 'shuffle-2c4p' SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8'
SHUFFLE_4C4P = 'shuffle-4c4p' SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8'
CHOICES = ( CHOICES = (
( (
@@ -1782,17 +1781,16 @@ class CableProfileChoices(ChoiceSet):
), ),
), ),
( (
_('Breakout'), _('Breakouts'),
( (
(BREAKOUT_1C4P_4C1P, _('Breakout (1C4P/4C1P)')), (BREAKOUT_1X4, _('Breakout (1:4)')),
(BREAKOUT_1C6P_6C1P, _('Breakout (1C6P/6C1P)')),
), ),
), ),
( (
_('Shuffle'), _('Shuffles'),
( (
(SHUFFLE_2C4P, _('Shuffle (2C4P)')), (SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')),
(SHUFFLE_4C4P, _('Shuffle (4C4P)')), (SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')),
(SHUFFLE_BREAKOUT_2X8, _('Shuffle breakout (2x8)')), (SHUFFLE_BREAKOUT_2X8, _('Shuffle breakout (2x8)')),
), ),
), ),

View File

@@ -156,10 +156,9 @@ class Cable(PrimaryModel):
CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile, CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile,
CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile, CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile,
CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile, CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile,
CableProfileChoices.BREAKOUT_1C4P_4C1P: cable_profiles.Breakout1C4Px4C1PCableProfile, CableProfileChoices.BREAKOUT_1X4: cable_profiles.Breakout1x4CableProfile,
CableProfileChoices.BREAKOUT_1C6P_6C1P: cable_profiles.Breakout1C6Px6C1PCableProfile, CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile,
CableProfileChoices.SHUFFLE_2C4P: cable_profiles.Shuffle2C4PCableProfile, CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile,
CableProfileChoices.SHUFFLE_4C4P: cable_profiles.Shuffle4C4PCableProfile,
CableProfileChoices.SHUFFLE_BREAKOUT_2X8: cable_profiles.ShuffleBreakout2x8CableProfile, CableProfileChoices.SHUFFLE_BREAKOUT_2X8: cable_profiles.ShuffleBreakout2x8CableProfile,
}.get(self.profile) }.get(self.profile)
@@ -364,7 +363,7 @@ class Cable(PrimaryModel):
connector = positions = None connector = positions = None
if self.profile: if self.profile:
connector = i connector = i
positions = list(range(1, self.profile_class().a_connectors[i] + 1)) positions = self.profile_class().a_connectors[i]
CableTermination( CableTermination(
cable=self, cable=self,
cable_end='A', cable_end='A',
@@ -377,7 +376,7 @@ class Cable(PrimaryModel):
connector = positions = None connector = positions = None
if self.profile: if self.profile:
connector = i connector = i
positions = list(range(1, self.profile_class().b_connectors[i] + 1)) positions = self.profile_class().b_connectors[i]
CableTermination( CableTermination(
cable=self, cable=self,
cable_end='B', cable_end='B',

View File

@@ -2586,7 +2586,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
'object_id': interfaces[14].pk, 'object_id': interfaces[14].pk,
}], }],
'label': 'Cable 4', 'label': 'Cable 4',
'profile': CableProfileChoices.SINGLE_1C1P, 'profile': CableProfileChoices.STRAIGHT_1C1P,
}, },
{ {
'a_terminations': [{ 'a_terminations': [{
@@ -2598,7 +2598,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
'object_id': interfaces[15].pk, 'object_id': interfaces[15].pk,
}], }],
'label': 'Cable 5', 'label': 'Cable 5',
'profile': CableProfileChoices.SINGLE_1C1P, 'profile': CableProfileChoices.STRAIGHT_1C1P,
}, },
{ {
'a_terminations': [{ 'a_terminations': [{

View File

@@ -559,7 +559,7 @@ class CablePathTests(CablePathTestCase):
cable4.clean() cable4.clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[rear_ports[0]], a_terminations=[rear_ports[0]],
b_terminations=interfaces[4:8], b_terminations=interfaces[4:8],
) )
@@ -632,7 +632,7 @@ class CablePathTests(CablePathTestCase):
# Test SVG generation # Test SVG generation
CableTraceSVG(interfaces[0]).render() CableTraceSVG(interfaces[0]).render()
def test_106_cable_profile_shuffle(self): def test_106_cable_profile_shuffle_2x2_mpo8(self):
""" """
[IF1] --C1-- [FP1][RP1] --C17-- [RP3][FP9] --C9-- [IF9] [IF1] --C1-- [FP1][RP1] --C17-- [RP3][FP9] --C9-- [IF9]
[IF2] --C2-- [FP2] [FP10] --C10-- [IF10] [IF2] --C2-- [FP2] [FP10] --C10-- [IF10]
@@ -710,7 +710,7 @@ class CablePathTests(CablePathTestCase):
cable.save() cable.save()
cables.append(cable) cables.append(cable)
shuffle_cable = Cable( shuffle_cable = Cable(
profile=CableProfileChoices.SHUFFLE_2C4P, profile=CableProfileChoices.SHUFFLE_2X2_MPO8,
a_terminations=rear_ports[0:2], a_terminations=rear_ports[0:2],
b_terminations=rear_ports[2:4], b_terminations=rear_ports[2:4],
) )
@@ -843,14 +843,14 @@ class CablePathTests(CablePathTestCase):
# Create cables # Create cables
cable1 = Cable( cable1 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[frontport1], a_terminations=[frontport1],
b_terminations=[interfaces[0], interfaces[1]], b_terminations=[interfaces[0], interfaces[1]],
) )
cable1.clean() cable1.clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[rearport1], a_terminations=[rearport1],
b_terminations=[interfaces[2], interfaces[3]] b_terminations=[interfaces[2], interfaces[3]]
) )
@@ -974,14 +974,14 @@ class CablePathTests(CablePathTestCase):
# Create cables # Create cables
cable1 = Cable( cable1 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[frontport1], a_terminations=[frontport1],
b_terminations=[interfaces[0], interfaces[1]], b_terminations=[interfaces[0], interfaces[1]],
) )
cable1.clean() cable1.clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[frontport2], a_terminations=[frontport2],
b_terminations=[interfaces[2], interfaces[3]], b_terminations=[interfaces[2], interfaces[3]],
) )
@@ -995,14 +995,14 @@ class CablePathTests(CablePathTestCase):
cable3.clean() cable3.clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[frontport3], a_terminations=[frontport3],
b_terminations=[interfaces[4], interfaces[5]], b_terminations=[interfaces[4], interfaces[5]],
) )
cable4.clean() cable4.clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[frontport4], a_terminations=[frontport4],
b_terminations=[interfaces[6], interfaces[7]], b_terminations=[interfaces[6], interfaces[7]],
) )
@@ -1115,14 +1115,14 @@ class CablePathTests(CablePathTestCase):
# Create cables # Create cables
cable1 = Cable( cable1 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[circuittermination1], a_terminations=[circuittermination1],
b_terminations=[interfaces[0], interfaces[1]], b_terminations=[interfaces[0], interfaces[1]],
) )
cable1.clean() cable1.clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
profile=CableProfileChoices.BREAKOUT_1C4P_4C1P, profile=CableProfileChoices.BREAKOUT_1X4,
a_terminations=[circuittermination2], a_terminations=[circuittermination2],
b_terminations=[interfaces[2], interfaces[3]] b_terminations=[interfaces[2], interfaces[3]]
) )