Additional work for FR #20788 (#20973)
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
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run

This commit is contained in:
Jeremy Stretch
2025-12-15 15:41:07 -05:00
committed by GitHub
parent 3140060f21
commit 875e3e7979
20 changed files with 1611 additions and 517 deletions

View File

@@ -353,7 +353,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
model = CircuitTermination model = CircuitTermination
fields = ( fields = (
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', '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): def search(self, queryset, name, value):

View File

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

View File

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

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_position'), ('circuits', '0054_cable_connector_positions'),
] ]
operations = [ operations = [

View File

@@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet filterset = CircuitTerminationFilterSet
ignore_fields = ('cable',) ignore_fields = ('cable', 'cable_positions')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@@ -61,11 +61,12 @@ class CableTerminationSerializer(NetBoxModelSerializer):
model = CableTermination model = CableTermination
fields = [ fields = [
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', '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 read_only_fields = fields
brief_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',
) )

View File

@@ -1,108 +1,390 @@
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:
# Maximum number of terminations allowed per side """Base class for representing a cable profile."""
a_max_connections = None
b_max_connections = None # 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): def clean(self, cable):
# Enforce maximum connection limits # Enforce maximum terminations limits
if self.a_max_connections and len(cable.a_terminations) > self.a_max_connections: 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({ raise ValidationError({
'a_terminations': _( '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( ).format(
count=a_terminations_count,
profile=cable.get_profile_display(), 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({ raise ValidationError({
'b_terminations': _( '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( ).format(
count=b_terminations_count,
profile=cable.get_profile_display(), 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. Return the mapped far-end connector & position for a given cable end the local connector & position.
By default, assume all positions are symmetrical.
""" """
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): def get_peer_termination(self, termination, position):
local_end = terminations[0].cable_end """
qs = CableTermination.objects.filter( Given a terminating object, return the peer terminating object (if any) on the opposite end of the cable.
cable=terminations[0].cable, """
cable_end=terminations[0].opposite_cable_end 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 @staticmethod
if position_stack: def get_position_list(n):
# Attempt to find a peer termination at the same position currently in the stack. Pop the stack only if """Return a list of integers from 1 to n, inclusive."""
# we find one. Otherwise, return any peer terminations with a null position. return list(range(1, n + 1))
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)
class StraightSingleCableProfile(BaseCableProfile): # Profile naming:
a_max_connections = 1 # - Single: One connector per side, with one or more positions
b_max_connections = 1 # - 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 StraightMultiCableProfile(BaseCableProfile): a_connectors = {
a_max_connections = None
b_max_connections = None
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
a_max_connections = 8
b_max_connections = 8
_mapping = {
1: 1, 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, 2: 2,
3: 5,
4: 6,
5: 3,
6: 4,
7: 7,
8: 8,
} }
b_connectors = a_connectors
def get_mapped_position(self, side, position):
return self._mapping.get(position)
class Shuffle4x4MPO8CableProfile(BaseCableProfile): class Trunk2C4PCableProfile(BaseCableProfile):
a_max_connections = 8 a_connectors = {
b_max_connections = 8 1: 4,
# A side to B side position mapping 2: 4,
_a_mapping = { }
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, 1: 1,
2: 3, 2: 1,
3: 5, 3: 1,
4: 7, 4: 1,
5: 2,
6: 4,
7: 6,
8: 8,
} }
# B side to A side position mapping (reverse of _a_mapping) b_connectors = a_connectors
_b_mapping = {v: k for k, v in _a_mapping.items()}
def get_mapped_position(self, side, position):
if side.lower() == 'b': class Trunk4C2PCableProfile(BaseCableProfile):
return self._b_mapping.get(position) a_connectors = {
return self._a_mapping.get(position) 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))

View File

@@ -1722,16 +1722,74 @@ class PortTypeChoices(ChoiceSet):
# #
class CableProfileChoices(ChoiceSet): class CableProfileChoices(ChoiceSet):
STRAIGHT_SINGLE = 'straight-single' # Singles
STRAIGHT_MULTI = 'straight-multi' SINGLE_1C1P = 'single-1c1p'
SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8' SINGLE_1C2P = 'single-1c2p'
SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8' 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 = ( CHOICES = (
(STRAIGHT_SINGLE, _('Straight (single position)')), (
(STRAIGHT_MULTI, _('Straight (multi-position)')), _('Single'),
(SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')), (
(SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')), (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)')),
),
),
) )

View File

@@ -24,6 +24,9 @@ RACK_STARTING_UNIT_DEFAULT = 1
# Cables # Cables
# #
CABLE_CONNECTOR_MIN = 1
CABLE_CONNECTOR_MAX = 256
CABLE_POSITION_MIN = 1 CABLE_POSITION_MIN = 1
CABLE_POSITION_MAX = 1024 CABLE_POSITION_MAX = 1024

View File

@@ -1748,7 +1748,9 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
class Meta: class Meta:
model = ConsolePort 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 @register_filterset
@@ -1760,7 +1762,9 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
class Meta: class Meta:
model = ConsoleServerPort 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 @register_filterset
@@ -1774,7 +1778,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
model = PowerPort model = PowerPort
fields = ( fields = (
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end', '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 model = PowerOutlet
fields = ( fields = (
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end', 'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
'cable_position', 'cable_connector',
) )
@@ -2111,7 +2115,7 @@ class InterfaceFilterSet(
fields = ( fields = (
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role', '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', '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): def filter_virtual_chassis_member_or_master(self, queryset, name, value):
@@ -2167,7 +2171,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
model = FrontPort model = FrontPort
fields = ( fields = (
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', '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 model = RearPort
fields = ( fields = (
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
'cable_position', 'cable_connector',
) )
@@ -2544,7 +2548,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = CableTermination model = CableTermination
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id') fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
@register_filterset @register_filterset
@@ -2663,7 +2667,7 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo
model = PowerFeed model = PowerFeed
fields = ( fields = (
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', '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): def search(self, queryset, name, value):

View File

@@ -1,3 +1,4 @@
import django.contrib.postgres.fields
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@@ -16,25 +17,40 @@ class Migration(migrations.Migration):
), ),
migrations.AddField( migrations.AddField(
model_name='cabletermination', model_name='cabletermination',
name='position', name='connector',
field=models.PositiveIntegerField( field=models.PositiveSmallIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=[ validators=[
django.core.validators.MinValueValidator(1), 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( migrations.AlterModelOptions(
name='cabletermination', name='cabletermination',
options={'ordering': ('cable', 'cable_end', 'position', 'pk')}, options={'ordering': ('cable', 'cable_end', 'connector', 'pk')}, # connector may be null
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='cabletermination', model_name='cabletermination',
constraint=models.UniqueConstraint( constraint=models.UniqueConstraint(
fields=('cable', 'cable_end', 'position'), fields=('cable', 'cable_end', 'connector'),
name='dcim_cabletermination_unique_position' name='dcim_cabletermination_unique_connector'
), ),
), ),
] ]

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ def populate_port_mappings(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0221_cable_position'), ('dcim', '0221_cable_connector_positions'),
] ]
operations = [ operations = [

View File

@@ -3,6 +3,7 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@@ -136,10 +137,30 @@ class Cable(PrimaryModel):
def profile_class(self): def profile_class(self):
from dcim import cable_profiles from dcim import cable_profiles
return { return {
CableProfileChoices.STRAIGHT_SINGLE: cable_profiles.StraightSingleCableProfile, CableProfileChoices.SINGLE_1C1P: cable_profiles.Single1C1PCableProfile,
CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile, CableProfileChoices.SINGLE_1C2P: cable_profiles.Single1C2PCableProfile,
CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile, CableProfileChoices.SINGLE_1C4P: cable_profiles.Single1C4PCableProfile,
CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile, 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) }.get(self.profile)
def _get_x_terminations(self, side): def _get_x_terminations(self, side):
@@ -338,14 +359,33 @@ class Cable(PrimaryModel):
ct.delete() ct.delete()
# Save any new CableTerminations # Save any new CableTerminations
profile = self.profile_class() if self.profile else None
for i, termination in enumerate(self.a_terminations, start=1): for i, termination in enumerate(self.a_terminations, start=1):
if not termination.pk or termination not in a_terminations: if not termination.pk or termination not in a_terminations:
position = i if self.profile and isinstance(termination, PathEndpoint) else None connector = positions = None
CableTermination(cable=self, cable_end='A', position=position, termination=termination).save() 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): for i, termination in enumerate(self.b_terminations, start=1):
if not termination.pk or termination not in b_terminations: if not termination.pk or termination not in b_terminations:
position = i if self.profile and isinstance(termination, PathEndpoint) else None connector = positions = None
CableTermination(cable=self, cable_end='B', position=position, termination=termination).save() 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): class CableTermination(ChangeLoggedModel):
@@ -372,13 +412,23 @@ class CableTermination(ChangeLoggedModel):
ct_field='termination_type', ct_field='termination_type',
fk_field='termination_id' fk_field='termination_id'
) )
position = models.PositiveIntegerField( connector = models.PositiveSmallIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=( validators=(
MinValueValidator(CABLE_POSITION_MIN), MinValueValidator(CABLE_CONNECTOR_MIN),
MaxValueValidator(CABLE_POSITION_MAX) 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 # Cached associations to enable efficient filtering
@@ -410,15 +460,15 @@ class CableTermination(ChangeLoggedModel):
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
class Meta: class Meta:
ordering = ('cable', 'cable_end', 'position', 'pk') ordering = ('cable', 'cable_end', 'connector', 'pk')
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('termination_type', 'termination_id'), fields=('termination_type', 'termination_id'),
name='%(app_label)s_%(class)s_unique_termination' name='%(app_label)s_%(class)s_unique_termination'
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('cable', 'cable_end', 'position'), fields=('cable', 'cable_end', 'connector'),
name='%(app_label)s_%(class)s_unique_position' name='%(app_label)s_%(class)s_unique_connector'
), ),
) )
verbose_name = _('cable termination') verbose_name = _('cable termination')
@@ -481,9 +531,7 @@ class CableTermination(ChangeLoggedModel):
# Set the cable on the terminating object # Set the cable on the terminating object
termination = self.termination._meta.model.objects.get(pk=self.termination_id) termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination.snapshot() termination.snapshot()
termination.cable = self.cable termination.set_cable_termination(self)
termination.cable_end = self.cable_end
termination.cable_position = self.position
termination.save() termination.save()
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
@@ -491,9 +539,7 @@ class CableTermination(ChangeLoggedModel):
# Delete the cable association on the terminating object # Delete the cable association on the terminating object
termination = self.termination._meta.model.objects.get(pk=self.termination_id) termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination.snapshot() termination.snapshot()
termination.cable = None termination.clear_cable_termination(self)
termination.cable_end = None
termination.cable_position = None
termination.save() termination.save()
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
@@ -701,9 +747,9 @@ class CablePath(models.Model):
path.append([ path.append([
object_to_path_node(t) for t in terminations object_to_path_node(t) for t in terminations
]) ])
# If not null, push cable_position onto the stack # If not null, push cable position onto the stack
if terminations[0].cable_position is not None: if isinstance(terminations[0], PathEndpoint) and terminations[0].cable_positions:
position_stack.append([terminations[0].cable_position]) position_stack.append([terminations[0].cable_positions[0]])
# Step 2: Determine the attached links (Cable or WirelessLink), if any # Step 2: Determine the attached links (Cable or WirelessLink), if any
links = list(dict.fromkeys( links = list(dict.fromkeys(
@@ -744,8 +790,9 @@ class CablePath(models.Model):
# Profile-based tracing # Profile-based tracing
if links[0].profile: if links[0].profile:
cable_profile = links[0].profile_class() cable_profile = links[0].profile_class()
peer_cable_terminations = cable_profile.get_peer_terminations(terminations, position_stack) term, position = cable_profile.get_peer_termination(terminations[0], position_stack.pop()[0])
remote_terminations = [ct.termination for ct in peer_cable_terminations] remote_terminations = [term]
position_stack.append([position])
# Legacy (positionless) behavior # Legacy (positionless) behavior
else: else:

View File

@@ -1,6 +1,7 @@
from functools import cached_property from functools import cached_property
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@@ -177,15 +178,24 @@ class CabledObjectModel(models.Model):
blank=True, blank=True,
null=True null=True
) )
cable_position = models.PositiveIntegerField( cable_connector = models.PositiveSmallIntegerField(
verbose_name=_('cable position'),
blank=True, blank=True,
null=True, null=True,
validators=( validators=(
MinValueValidator(CABLE_POSITION_MIN), MinValueValidator(CABLE_CONNECTOR_MIN),
MaxValueValidator(CABLE_POSITION_MAX) 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( mark_connected = models.BooleanField(
verbose_name=_('mark connected'), verbose_name=_('mark connected'),
default=False, default=False,
@@ -210,18 +220,31 @@ class CabledObjectModel(models.Model):
raise ValidationError({ raise ValidationError({
"cable_end": _("Must specify cable end (A or B) when attaching a cable.") "cable_end": _("Must specify cable end (A or B) when attaching a cable.")
}) })
if self.cable_end and not self.cable: if self.cable_connector and not self.cable_positions:
raise ValidationError({ raise ValidationError({
"cable_end": _("Cable end must not be set without a cable.") "cable_positions": _("Must specify position(s) when specifying a cable connector.")
}) })
if self.cable_position and not self.cable: if self.cable_positions and not self.cable_connector:
raise ValidationError({ raise ValidationError({
"cable_position": _("Cable termination position must not be set without a cable.") "cable_positions": _("Cable positions cannot be set without a cable connector.")
}) })
if self.mark_connected and self.cable: if self.mark_connected:
raise ValidationError({ raise ValidationError({
"mark_connected": _("Cannot mark as connected with a cable attached.") "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 @property
def link(self): def link(self):
@@ -256,6 +279,22 @@ class CabledObjectModel(models.Model):
return None return None
return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B 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): class PathEndpoint(models.Model):
""" """

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.STRAIGHT_SINGLE, 'profile': CableProfileChoices.SINGLE_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.STRAIGHT_SINGLE, 'profile': CableProfileChoices.SINGLE_1C1P,
}, },
{ {
'a_terminations': [{ 'a_terminations': [{
@@ -2620,7 +2620,9 @@ class CableTerminationTest(
APIViewTestCases.ListObjectsViewTestCase, APIViewTestCases.ListObjectsViewTestCase,
): ):
model = CableTermination 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 @classmethod
def setUpTestData(cls): def setUpTestData(cls):

File diff suppressed because it is too large Load Diff

View File

@@ -3332,6 +3332,7 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
filterset = ConsolePortFilterSet filterset = ConsolePortFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -3582,6 +3583,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
filterset = ConsoleServerPortFilterSet filterset = ConsoleServerPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -3832,6 +3834,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
filterset = PowerPortFilterSet filterset = PowerPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -4096,6 +4099,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
filterset = PowerOutletFilterSet filterset = PowerOutletFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -4380,7 +4384,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = Interface.objects.all() queryset = Interface.objects.all()
filterset = InterfaceFilterSet filterset = InterfaceFilterSet
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs') ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs', 'cable_positions')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -5017,6 +5021,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
filterset = FrontPortFilterSet filterset = FrontPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -5321,6 +5326,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
filterset = RearPortFilterSet filterset = RearPortFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -6859,6 +6865,7 @@ class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests): class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = PowerFeed.objects.all() queryset = PowerFeed.objects.all()
filterset = PowerFeedFilterSet filterset = PowerFeedFilterSet
ignore_fields = ('cable_positions',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@@ -41,12 +41,12 @@ def create_cablepaths(objects):
""" """
from dcim.models import CablePath 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) origins = defaultdict(list)
for obj in objects: 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): if cp := CablePath.from_origin(objects):
cp.save() cp.save()