mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-19 11:52:22 -06:00
@@ -21,6 +21,21 @@ The cable's operational status. Choices include:
|
||||
* Planned
|
||||
* Decommissioning
|
||||
|
||||
### Profile
|
||||
|
||||
!!! note "This field was introduced in NetBox v4.5."
|
||||
|
||||
The profile to which the cable conforms. The profile determines the mapping of termination between the two ends and enables logical tracing across complex connections, such as breakout cables. Supported profiles are listed below.
|
||||
|
||||
* Straight (single position)
|
||||
* Straight (multi-position)
|
||||
* Shuffle (2x2 MPO8)
|
||||
* Shuffle (4x4 MPO8)
|
||||
|
||||
A single-position cable is allowed only one termination point at each end. There is no limit to the number of terminations a multi-position cable may have. Each end of a cable must have the same number of terminations, unless connected to a pass-through port or to a circuit termination.
|
||||
|
||||
The assignment of a cable profile is optional. If no profile is assigned, legacy tracing behavior will be preserved.
|
||||
|
||||
### Type
|
||||
|
||||
The cable's physical medium or classification.
|
||||
|
||||
@@ -346,7 +346,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
||||
'mark_connected', 'pp_info', 'cable_end',
|
||||
'mark_connected', 'pp_info', 'cable_end', 'cable_position',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
||||
23
netbox/circuits/migrations/0054_cable_position.py
Normal file
23
netbox/circuits/migrations/0054_cable_position.py
Normal file
@@ -0,0 +1,23 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -25,15 +25,16 @@ class CableSerializer(PrimaryModelSerializer):
|
||||
a_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||
profile = ChoiceField(choices=CableProfileChoices, required=False)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant',
|
||||
'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'profile',
|
||||
'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'label', 'description')
|
||||
|
||||
@@ -60,10 +61,12 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
model = CableTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
|
||||
'termination', 'created', 'last_updated',
|
||||
'termination', 'position', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = fields
|
||||
brief_fields = ('id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id')
|
||||
brief_fields = (
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id',
|
||||
)
|
||||
|
||||
|
||||
class CablePathSerializer(serializers.ModelSerializer):
|
||||
|
||||
108
netbox/dcim/cable_profiles.py
Normal file
108
netbox/dcim/cable_profiles.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import CableTermination
|
||||
|
||||
|
||||
class BaseCableProfile:
|
||||
# Maximum number of terminations allowed per side
|
||||
a_max_connections = None
|
||||
b_max_connections = None
|
||||
|
||||
def clean(self, cable):
|
||||
# Enforce maximum connection limits
|
||||
if self.a_max_connections and len(cable.a_terminations) > self.a_max_connections:
|
||||
raise ValidationError({
|
||||
'a_terminations': _(
|
||||
'Maximum A side connections for profile {profile}: {max}'
|
||||
).format(
|
||||
profile=cable.get_profile_display(),
|
||||
max=self.a_max_connections,
|
||||
)
|
||||
})
|
||||
if self.b_max_connections and len(cable.b_terminations) > self.b_max_connections:
|
||||
raise ValidationError({
|
||||
'b_terminations': _(
|
||||
'Maximum B side connections for profile {profile}: {max}'
|
||||
).format(
|
||||
profile=cable.get_profile_display(),
|
||||
max=self.b_max_connections,
|
||||
)
|
||||
})
|
||||
|
||||
def get_mapped_position(self, side, position):
|
||||
"""
|
||||
Return the mapped position for a given cable end and position.
|
||||
|
||||
By default, assume all positions are symmetrical.
|
||||
"""
|
||||
return 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
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
class StraightSingleCableProfile(BaseCableProfile):
|
||||
a_max_connections = 1
|
||||
b_max_connections = 1
|
||||
|
||||
|
||||
class StraightMultiCableProfile(BaseCableProfile):
|
||||
a_max_connections = None
|
||||
b_max_connections = None
|
||||
|
||||
|
||||
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
|
||||
a_max_connections = 8
|
||||
b_max_connections = 8
|
||||
_mapping = {
|
||||
1: 1,
|
||||
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)
|
||||
|
||||
|
||||
class Shuffle4x4MPO8CableProfile(BaseCableProfile):
|
||||
a_max_connections = 8
|
||||
b_max_connections = 8
|
||||
# A side to B side position mapping
|
||||
_a_mapping = {
|
||||
1: 1,
|
||||
2: 3,
|
||||
3: 5,
|
||||
4: 7,
|
||||
5: 2,
|
||||
6: 4,
|
||||
7: 6,
|
||||
8: 8,
|
||||
}
|
||||
# B side to A side position mapping (reverse of _a_mapping)
|
||||
_b_mapping = {v: k for k, v in _a_mapping.items()}
|
||||
|
||||
def get_mapped_position(self, side, position):
|
||||
if side.lower() == 'b':
|
||||
return self._b_mapping.get(position)
|
||||
return self._a_mapping.get(position)
|
||||
@@ -1717,6 +1717,19 @@ class PortTypeChoices(ChoiceSet):
|
||||
# Cables/links
|
||||
#
|
||||
|
||||
class CableProfileChoices(ChoiceSet):
|
||||
STRAIGHT_SINGLE = 'straight-single'
|
||||
STRAIGHT_MULTI = 'straight-multi'
|
||||
SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8'
|
||||
SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8'
|
||||
|
||||
CHOICES = (
|
||||
(STRAIGHT_SINGLE, _('Straight (single position)')),
|
||||
(STRAIGHT_MULTI, _('Straight (multi-position)')),
|
||||
(SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')),
|
||||
(SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')),
|
||||
)
|
||||
|
||||
|
||||
class CableTypeChoices(ChoiceSet):
|
||||
# Copper - Twisted Pair (UTP/STP)
|
||||
|
||||
@@ -20,6 +20,14 @@ RACK_ELEVATION_DEFAULT_MARGIN_WIDTH = 15
|
||||
RACK_STARTING_UNIT_DEFAULT = 1
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
CABLE_POSITION_MIN = 1
|
||||
CABLE_POSITION_MAX = 1024
|
||||
|
||||
|
||||
#
|
||||
# RearPorts
|
||||
#
|
||||
|
||||
@@ -1699,7 +1699,7 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
@@ -1710,7 +1710,7 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||
|
||||
|
||||
class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
@@ -1723,6 +1723,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
)
|
||||
|
||||
|
||||
@@ -1748,6 +1749,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
)
|
||||
|
||||
|
||||
@@ -2055,7 +2057,7 @@ class InterfaceFilterSet(
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
|
||||
'cable_id', 'cable_end',
|
||||
'cable_id', 'cable_end', 'cable_position',
|
||||
)
|
||||
|
||||
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
|
||||
@@ -2107,6 +2109,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
)
|
||||
|
||||
|
||||
@@ -2120,6 +2123,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
)
|
||||
|
||||
|
||||
@@ -2316,6 +2320,9 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=LinkStatusChoices
|
||||
)
|
||||
profile = django_filters.MultipleChoiceFilter(
|
||||
choices=CableProfileChoices
|
||||
)
|
||||
color = django_filters.MultipleChoiceFilter(
|
||||
choices=ColorChoices
|
||||
)
|
||||
@@ -2465,7 +2472,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CableTermination
|
||||
fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
|
||||
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
|
||||
|
||||
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
@@ -2582,7 +2589,7 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
|
||||
'available_power', 'mark_connected', 'cable_end', 'description',
|
||||
'available_power', 'mark_connected', 'cable_end', 'cable_position', 'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
||||
@@ -780,6 +780,12 @@ class CableBulkEditForm(PrimaryModelBulkEditForm):
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
profile = forms.ChoiceField(
|
||||
label=_('Profile'),
|
||||
choices=add_blank_choice(CableProfileChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
@@ -808,11 +814,11 @@ class CableBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
model = Cable
|
||||
fieldsets = (
|
||||
FieldSet('type', 'status', 'tenant', 'label', 'description'),
|
||||
FieldSet('type', 'status', 'profile', 'tenant', 'label', 'description'),
|
||||
FieldSet('color', 'length', 'length_unit', name=_('Attributes')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
|
||||
'type', 'status', 'profile', 'tenant', 'label', 'color', 'length', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1461,6 +1461,12 @@ class CableImportForm(PrimaryModelImportForm):
|
||||
required=False,
|
||||
help_text=_('Connection status')
|
||||
)
|
||||
profile = CSVChoiceField(
|
||||
label=_('Profile'),
|
||||
choices=CableProfileChoices,
|
||||
required=False,
|
||||
help_text=_('Cable connection profile')
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
label=_('Type'),
|
||||
choices=CableTypeChoices,
|
||||
@@ -1491,8 +1497,8 @@ class CableImportForm(PrimaryModelImportForm):
|
||||
model = Cable
|
||||
fields = [
|
||||
'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
|
||||
'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
|
||||
'owner', 'comments', 'tags',
|
||||
'side_b_name', 'type', 'status', 'profile', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||
'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
||||
@@ -1119,7 +1119,7 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
|
||||
FieldSet('type', 'status', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
||||
FieldSet('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -1175,6 +1175,11 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
required=False,
|
||||
choices=add_blank_choice(LinkStatusChoices)
|
||||
)
|
||||
profile = forms.MultipleChoiceField(
|
||||
label=_('Profile'),
|
||||
required=False,
|
||||
choices=add_blank_choice(CableProfileChoices)
|
||||
)
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
|
||||
@@ -807,8 +807,8 @@ class CableForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
|
||||
'a_terminations_type', 'b_terminations_type', 'type', 'status', 'profile', 'tenant_group', 'tenant',
|
||||
'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db import connection
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
||||
from dcim.signals import create_cablepath
|
||||
from dcim.signals import create_cablepaths
|
||||
|
||||
ENDPOINT_MODELS = (
|
||||
ConsolePort,
|
||||
@@ -81,7 +81,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(f'Retracing {origins_count} cabled {model._meta.verbose_name_plural}...')
|
||||
i = 0
|
||||
for i, obj in enumerate(origins, start=1):
|
||||
create_cablepath([obj])
|
||||
create_cablepaths([obj])
|
||||
if not i % 100:
|
||||
self.draw_progress_bar(i * 100 / origins_count)
|
||||
self.draw_progress_bar(100)
|
||||
|
||||
40
netbox/dcim/migrations/0219_cable_profile.py
Normal file
40
netbox/dcim/migrations/0219_cable_profile.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0218_devicetype_device_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cable',
|
||||
name='profile',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cabletermination',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cabletermination',
|
||||
options={'ordering': ('cable', 'cable_end', 'position', 'pk')},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='cabletermination',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('cable', 'cable_end', 'position'),
|
||||
name='dcim_cabletermination_unique_position'
|
||||
),
|
||||
),
|
||||
]
|
||||
107
netbox/dcim/migrations/0220_cable_position.py
Normal file
107
netbox/dcim/migrations/0220_cable_position.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0219_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),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -3,6 +3,7 @@ import itertools
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.dispatch import Signal
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -20,7 +21,7 @@ from utilities.fields import ColorField, GenericArrayForeignKey
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.serialization import deserialize_object, serialize_object
|
||||
from wireless.models import WirelessLink
|
||||
from .device_components import FrontPort, RearPort, PathEndpoint
|
||||
from .device_components import FrontPort, PathEndpoint, RearPort
|
||||
|
||||
__all__ = (
|
||||
'Cable',
|
||||
@@ -54,6 +55,12 @@ class Cable(PrimaryModel):
|
||||
choices=LinkStatusChoices,
|
||||
default=LinkStatusChoices.STATUS_CONNECTED
|
||||
)
|
||||
profile = models.CharField(
|
||||
verbose_name=_('profile'),
|
||||
max_length=50,
|
||||
choices=CableProfileChoices,
|
||||
blank=True,
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -92,7 +99,7 @@ class Cable(PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
clone_fields = ('tenant', 'type',)
|
||||
clone_fields = ('tenant', 'type', 'profile')
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
@@ -123,6 +130,16 @@ class Cable(PrimaryModel):
|
||||
def get_status_color(self):
|
||||
return LinkStatusChoices.colors.get(self.status)
|
||||
|
||||
@property
|
||||
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,
|
||||
}.get(self.profile)
|
||||
|
||||
def _get_x_terminations(self, side):
|
||||
"""
|
||||
Return the terminating objects for the given cable end (A or B).
|
||||
@@ -195,6 +212,10 @@ class Cable(PrimaryModel):
|
||||
if self._state.adding and self.pk is None and (not self.a_terminations or not self.b_terminations):
|
||||
raise ValidationError(_("Must define A and B terminations when creating a new cable."))
|
||||
|
||||
# Validate terminations against the assigned cable profile (if any)
|
||||
if self.profile:
|
||||
self.profile_class().clean(self)
|
||||
|
||||
if self._terminations_modified:
|
||||
|
||||
# Check that all termination objects for either end are of the same type
|
||||
@@ -315,12 +336,14 @@ class Cable(PrimaryModel):
|
||||
ct.delete()
|
||||
|
||||
# Save any new CableTerminations
|
||||
for termination in self.a_terminations:
|
||||
for i, termination in enumerate(self.a_terminations, start=1):
|
||||
if not termination.pk or termination not in a_terminations:
|
||||
CableTermination(cable=self, cable_end='A', termination=termination).save()
|
||||
for termination in self.b_terminations:
|
||||
position = i if self.profile and isinstance(termination, PathEndpoint) else None
|
||||
CableTermination(cable=self, cable_end='A', position=position, termination=termination).save()
|
||||
for i, termination in enumerate(self.b_terminations, start=1):
|
||||
if not termination.pk or termination not in b_terminations:
|
||||
CableTermination(cable=self, cable_end='B', termination=termination).save()
|
||||
position = i if self.profile and isinstance(termination, PathEndpoint) else None
|
||||
CableTermination(cable=self, cable_end='B', position=position, termination=termination).save()
|
||||
|
||||
|
||||
class CableTermination(ChangeLoggedModel):
|
||||
@@ -347,6 +370,14 @@ class CableTermination(ChangeLoggedModel):
|
||||
ct_field='termination_type',
|
||||
fk_field='termination_id'
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=(
|
||||
MinValueValidator(CABLE_POSITION_MIN),
|
||||
MaxValueValidator(CABLE_POSITION_MAX)
|
||||
)
|
||||
)
|
||||
|
||||
# Cached associations to enable efficient filtering
|
||||
_device = models.ForeignKey(
|
||||
@@ -377,12 +408,16 @@ class CableTermination(ChangeLoggedModel):
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('cable', 'cable_end', 'pk')
|
||||
ordering = ('cable', 'cable_end', 'position', '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'
|
||||
),
|
||||
)
|
||||
verbose_name = _('cable termination')
|
||||
verbose_name_plural = _('cable terminations')
|
||||
@@ -446,6 +481,7 @@ class CableTermination(ChangeLoggedModel):
|
||||
termination.snapshot()
|
||||
termination.cable = self.cable
|
||||
termination.cable_end = self.cable_end
|
||||
termination.cable_position = self.position
|
||||
termination.save()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
@@ -455,6 +491,7 @@ class CableTermination(ChangeLoggedModel):
|
||||
termination.snapshot()
|
||||
termination.cable = None
|
||||
termination.cable_end = None
|
||||
termination.cable_position = None
|
||||
termination.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -653,6 +690,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])
|
||||
|
||||
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
||||
links = [termination.link for termination in terminations if termination.link is not None]
|
||||
@@ -687,6 +727,14 @@ class CablePath(models.Model):
|
||||
|
||||
# Step 6: Determine the far-end terminations
|
||||
if isinstance(links[0], Cable):
|
||||
# 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]
|
||||
|
||||
# Legacy (positionless) behavior
|
||||
else:
|
||||
termination_type = ObjectType.objects.get_for_model(terminations[0])
|
||||
local_cable_terminations = CableTermination.objects.filter(
|
||||
termination_type=termination_type,
|
||||
|
||||
@@ -175,6 +175,15 @@ class CabledObjectModel(models.Model):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
cable_position = models.PositiveIntegerField(
|
||||
verbose_name=_('cable position'),
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=(
|
||||
MinValueValidator(CABLE_POSITION_MIN),
|
||||
MaxValueValidator(CABLE_POSITION_MAX)
|
||||
),
|
||||
)
|
||||
mark_connected = models.BooleanField(
|
||||
verbose_name=_('mark connected'),
|
||||
default=False,
|
||||
@@ -194,14 +203,23 @@ class CabledObjectModel(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.cable and not self.cable_end:
|
||||
if self.cable:
|
||||
if not self.cable_end:
|
||||
raise ValidationError({
|
||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||
})
|
||||
if not self.cable_position:
|
||||
raise ValidationError({
|
||||
"cable_position": _("Must specify cable termination position 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.")
|
||||
|
||||
@@ -11,7 +11,7 @@ from .models import (
|
||||
VirtualChassis,
|
||||
)
|
||||
from .models.cables import trace_paths
|
||||
from .utils import create_cablepath, rebuild_paths
|
||||
from .utils import create_cablepaths, rebuild_paths
|
||||
|
||||
COMPONENT_MODELS = (
|
||||
ConsolePort,
|
||||
@@ -114,7 +114,7 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
||||
if not nodes:
|
||||
continue
|
||||
if isinstance(nodes[0], PathEndpoint):
|
||||
create_cablepath(nodes)
|
||||
create_cablepaths(nodes)
|
||||
else:
|
||||
rebuild_paths(nodes)
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ class CableTable(TenancyColumnsMixin, PrimaryModelTable):
|
||||
verbose_name=_('Site B')
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
profile = columns.ChoiceFieldColumn()
|
||||
length = columns.TemplateColumn(
|
||||
template_code=CABLE_LENGTH,
|
||||
order_by=('_abs_length')
|
||||
@@ -125,8 +126,8 @@ class CableTable(TenancyColumnsMixin, PrimaryModelTable):
|
||||
model = Cable
|
||||
fields = (
|
||||
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',
|
||||
'location_a', 'location_b', 'site_a', 'site_b', 'status', 'type', 'tenant', 'tenant_group', 'color',
|
||||
'color_name', 'length', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'location_a', 'location_b', 'site_a', 'site_b', 'status', 'profile', 'type', 'tenant', 'tenant_group',
|
||||
'color', 'color_name', 'length', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type',
|
||||
|
||||
@@ -2396,6 +2396,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
'object_id': interfaces[14].pk,
|
||||
}],
|
||||
'label': 'Cable 4',
|
||||
'profile': CableProfileChoices.STRAIGHT_SINGLE,
|
||||
},
|
||||
{
|
||||
'a_terminations': [{
|
||||
@@ -2407,6 +2408,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
'object_id': interfaces[15].pk,
|
||||
}],
|
||||
'label': 'Cable 5',
|
||||
'profile': CableProfileChoices.STRAIGHT_SINGLE,
|
||||
},
|
||||
{
|
||||
'a_terminations': [{
|
||||
@@ -2418,6 +2420,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
'object_id': interfaces[16].pk,
|
||||
}],
|
||||
'label': 'Cable 6',
|
||||
# No profile (legacy behavior)
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2427,7 +2430,7 @@ class CableTerminationTest(
|
||||
APIViewTestCases.ListObjectsViewTestCase,
|
||||
):
|
||||
model = CableTermination
|
||||
brief_fields = ['cable', 'cable_end', 'display', 'id', 'termination_id', 'termination_type', 'url']
|
||||
brief_fields = ['cable', 'cable_end', 'display', 'id', 'position', 'termination_id', 'termination_type', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -1,100 +1,21 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import *
|
||||
from dcim.choices import LinkStatusChoices
|
||||
from dcim.models import *
|
||||
from dcim.svg import CableTraceSVG
|
||||
from dcim.utils import object_to_path_node
|
||||
from dcim.tests.utils import CablePathTestCase
|
||||
from utilities.exceptions import AbortRequest
|
||||
|
||||
|
||||
class CablePathTestCase(TestCase):
|
||||
class LegacyCablePathTests(CablePathTestCase):
|
||||
"""
|
||||
Test NetBox's ability to trace and retrace CablePaths in response to data model changes. Tests are numbered
|
||||
as follows:
|
||||
Test NetBox's ability to trace and retrace CablePaths in response to data model changes, without cable profiles.
|
||||
|
||||
Tests are numbered as follows:
|
||||
1XX: Test direct connections between different endpoint types
|
||||
2XX: Test different cable topologies
|
||||
3XX: Test responses to changes in existing objects
|
||||
4XX: Test to exclude specific cable topologies
|
||||
"""
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Create a single device that will hold all components
|
||||
cls.site = Site.objects.create(name='Site', slug='site')
|
||||
|
||||
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device')
|
||||
role = DeviceRole.objects.create(name='Device Role', slug='device-role')
|
||||
cls.device = Device.objects.create(site=cls.site, device_type=device_type, role=role, name='Test Device')
|
||||
|
||||
cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel')
|
||||
|
||||
provider = Provider.objects.create(name='Provider', slug='provider')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
|
||||
cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
|
||||
|
||||
def _get_cablepath(self, nodes, **kwargs):
|
||||
"""
|
||||
Return a given cable path
|
||||
|
||||
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
|
||||
|
||||
:return: The matching CablePath (if any)
|
||||
"""
|
||||
path = []
|
||||
for step in nodes:
|
||||
if type(step) in (list, tuple):
|
||||
path.append([object_to_path_node(node) for node in step])
|
||||
else:
|
||||
path.append([object_to_path_node(step)])
|
||||
return CablePath.objects.filter(path=path, **kwargs).first()
|
||||
|
||||
def assertPathExists(self, nodes, **kwargs):
|
||||
"""
|
||||
Assert that a CablePath from origin to destination with a specific intermediate path exists. Returns the
|
||||
first matching CablePath, if found.
|
||||
|
||||
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
|
||||
"""
|
||||
cablepath = self._get_cablepath(nodes, **kwargs)
|
||||
self.assertIsNotNone(cablepath, msg='CablePath not found')
|
||||
|
||||
return cablepath
|
||||
|
||||
def assertPathDoesNotExist(self, nodes, **kwargs):
|
||||
"""
|
||||
Assert that a specific CablePath does *not* exist.
|
||||
|
||||
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
|
||||
"""
|
||||
cablepath = self._get_cablepath(nodes, **kwargs)
|
||||
self.assertIsNone(cablepath, msg='Unexpected CablePath found')
|
||||
|
||||
def assertPathIsSet(self, origin, cablepath, msg=None):
|
||||
"""
|
||||
Assert that a specific CablePath instance is set as the path on the origin.
|
||||
|
||||
:param origin: The originating path endpoint
|
||||
:param cablepath: The CablePath instance originating from this endpoint
|
||||
:param msg: Custom failure message (optional)
|
||||
"""
|
||||
if msg is None:
|
||||
msg = f"Path #{cablepath.pk} not set on originating endpoint {origin}"
|
||||
self.assertEqual(origin._path_id, cablepath.pk, msg=msg)
|
||||
|
||||
def assertPathIsNotSet(self, origin, msg=None):
|
||||
"""
|
||||
Assert that a specific CablePath instance is set as the path on the origin.
|
||||
|
||||
:param origin: The originating path endpoint
|
||||
:param msg: Custom failure message (optional)
|
||||
"""
|
||||
if msg is None:
|
||||
msg = f"Path #{origin._path_id} set as origin on {origin}; should be None!"
|
||||
self.assertIsNone(origin._path_id, msg=msg)
|
||||
|
||||
def test_101_interface_to_interface(self):
|
||||
"""
|
||||
[IF1] --C1-- [IF2]
|
||||
@@ -2270,6 +2191,55 @@ class CablePathTestCase(TestCase):
|
||||
CableTraceSVG(interface1).render()
|
||||
CableTraceSVG(interface2).render()
|
||||
|
||||
def test_223_single_path_via_multiple_pass_throughs_with_breakouts(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF3]
|
||||
[IF2] [FP2] [RP2] [IF4]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1, interface2],
|
||||
b_terminations=[frontport1, frontport2]
|
||||
)
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
a_terminations=[rearport1, rearport2],
|
||||
b_terminations=[interface3, interface4]
|
||||
)
|
||||
cable2.save()
|
||||
|
||||
# Validate paths
|
||||
self.assertPathExists(
|
||||
(
|
||||
[interface1, interface2], cable1, [frontport1, frontport2],
|
||||
[rearport1, rearport2], cable2, [interface3, interface4],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(
|
||||
[interface3, interface4], cable2, [rearport1, rearport2],
|
||||
[frontport1, frontport2], cable1, [interface1, interface2],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
def test_301_create_path_via_existing_cable(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||
|
||||
788
netbox/dcim/tests/test_cablepaths2.py
Normal file
788
netbox/dcim/tests/test_cablepaths2.py
Normal file
@@ -0,0 +1,788 @@
|
||||
from unittest import skipIf
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.choices import CableProfileChoices
|
||||
from dcim.models import *
|
||||
from dcim.svg import CableTraceSVG
|
||||
from dcim.tests.utils import CablePathTestCase
|
||||
|
||||
|
||||
class CablePathTests(CablePathTestCase):
|
||||
"""
|
||||
Test the creation of CablePaths for Cables with different profiles applied.
|
||||
|
||||
Tests are numbered as follows:
|
||||
1XX: Test direct connections using each profile
|
||||
2XX: Topology tests replicated from the legacy test case and adapted to use profiles
|
||||
"""
|
||||
|
||||
def test_101_cable_profile_straight_single(self):
|
||||
"""
|
||||
[IF1] --C1-- [IF2]
|
||||
|
||||
Cable profile: Straight single
|
||||
"""
|
||||
interfaces = [
|
||||
Interface.objects.create(device=self.device, name='Interface 1'),
|
||||
Interface.objects.create(device=self.device, name='Interface 2'),
|
||||
]
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_SINGLE,
|
||||
a_terminations=[interfaces[0]],
|
||||
b_terminations=[interfaces[1]],
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
|
||||
path1 = self.assertPathExists(
|
||||
(interfaces[0], cable1, interfaces[1]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
path2 = self.assertPathExists(
|
||||
(interfaces[1], cable1, interfaces[0]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
interfaces[0].refresh_from_db()
|
||||
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)
|
||||
|
||||
# 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_102_cable_profile_straight_multi(self):
|
||||
"""
|
||||
[IF1] --C1-- [IF3]
|
||||
[IF2] [IF4]
|
||||
|
||||
Cable profile: Straight multi
|
||||
"""
|
||||
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'),
|
||||
]
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[0], interfaces[1]],
|
||||
b_terminations=[interfaces[2], interfaces[3]],
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
|
||||
path1 = self.assertPathExists(
|
||||
(interfaces[0], cable1, interfaces[2]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
path2 = self.assertPathExists(
|
||||
(interfaces[1], cable1, interfaces[3]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
path3 = self.assertPathExists(
|
||||
(interfaces[2], cable1, interfaces[0]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
path4 = self.assertPathExists(
|
||||
(interfaces[3], cable1, interfaces[1]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 4)
|
||||
|
||||
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.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)
|
||||
|
||||
# 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_103_cable_profile_2x2_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 (2x2 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 1:3'),
|
||||
Interface.objects.create(device=self.device, name='Interface 1:4'),
|
||||
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 2:3'),
|
||||
Interface.objects.create(device=self.device, name='Interface 2:4'),
|
||||
# B side
|
||||
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 3:3'),
|
||||
Interface.objects.create(device=self.device, name='Interface 3:4'),
|
||||
Interface.objects.create(device=self.device, name='Interface 4:1'),
|
||||
Interface.objects.create(device=self.device, name='Interface 4:2'),
|
||||
Interface.objects.create(device=self.device, name='Interface 4:3'),
|
||||
Interface.objects.create(device=self.device, name='Interface 4:4'),
|
||||
]
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.SHUFFLE_2X2_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[9]), 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[13]), is_complete=True, is_active=True
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[4], cable1, interfaces[10]), 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[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
|
||||
),
|
||||
]
|
||||
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_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]
|
||||
[IF2] [IF4]
|
||||
"""
|
||||
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'),
|
||||
]
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device,
|
||||
name='Front Port 1',
|
||||
rear_port=rearport1,
|
||||
rear_port_position=1
|
||||
)
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[0], interfaces[1]],
|
||||
b_terminations=[frontport1],
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[rearport1],
|
||||
b_terminations=[interfaces[2], interfaces[3]]
|
||||
)
|
||||
cable2.clean()
|
||||
cable2.save()
|
||||
|
||||
paths = [
|
||||
self.assertPathExists(
|
||||
(interfaces[0], cable1, frontport1, rearport1, cable2, interfaces[2]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[1], cable1, frontport1, rearport1, cable2, interfaces[3]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[2], cable2, rearport1, frontport1, cable1, interfaces[0]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[3], cable2, rearport1, frontport1, cable1, interfaces[1]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
),
|
||||
]
|
||||
self.assertEqual(CablePath.objects.count(), 4)
|
||||
for interface in interfaces:
|
||||
interface.refresh_from_db()
|
||||
self.assertPathIsSet(interfaces[0], paths[0])
|
||||
self.assertPathIsSet(interfaces[1], paths[1])
|
||||
self.assertPathIsSet(interfaces[2], paths[2])
|
||||
self.assertPathIsSet(interfaces[3], paths[3])
|
||||
|
||||
# Test SVG generation
|
||||
CableTraceSVG(interfaces[0]).render()
|
||||
|
||||
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]
|
||||
"""
|
||||
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'),
|
||||
]
|
||||
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', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport1_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
|
||||
)
|
||||
frontport2_1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
frontport2_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
||||
)
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[0], interfaces[1]],
|
||||
b_terminations=[frontport1_1]
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[2], interfaces[3]],
|
||||
b_terminations=[frontport1_2]
|
||||
)
|
||||
cable2.clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_SINGLE,
|
||||
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]]
|
||||
)
|
||||
cable4.clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[frontport2_2],
|
||||
b_terminations=[interfaces[6], interfaces[7]]
|
||||
)
|
||||
cable5.clean()
|
||||
cable5.save()
|
||||
|
||||
paths = [
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[0], cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4,
|
||||
interfaces[4],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[1], cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4,
|
||||
interfaces[5],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[2], cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5,
|
||||
interfaces[6],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[3], cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5,
|
||||
interfaces[7],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[4], cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1,
|
||||
interfaces[0],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[5], cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1,
|
||||
interfaces[1],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[6], cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2,
|
||||
interfaces[2],
|
||||
),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[7], cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2,
|
||||
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], paths[0])
|
||||
self.assertPathIsSet(interfaces[1], paths[1])
|
||||
self.assertPathIsSet(interfaces[2], paths[2])
|
||||
self.assertPathIsSet(interfaces[3], paths[3])
|
||||
self.assertPathIsSet(interfaces[4], paths[4])
|
||||
self.assertPathIsSet(interfaces[5], paths[5])
|
||||
self.assertPathIsSet(interfaces[6], paths[6])
|
||||
self.assertPathIsSet(interfaces[7], paths[7])
|
||||
|
||||
# Test SVG generation
|
||||
CableTraceSVG(interfaces[0]).render()
|
||||
|
||||
def test_212_interface_to_interface_via_circuit_with_breakouts(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1] [CT2] --C2-- [IF3]
|
||||
[IF2] [IF4]
|
||||
"""
|
||||
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'),
|
||||
]
|
||||
circuittermination1 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit,
|
||||
termination=self.site,
|
||||
term_side='A'
|
||||
)
|
||||
circuittermination2 = CircuitTermination.objects.create(
|
||||
circuit=self.circuit,
|
||||
termination=self.site,
|
||||
term_side='Z'
|
||||
)
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[0], interfaces[1]],
|
||||
b_terminations=[circuittermination1]
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[circuittermination2],
|
||||
b_terminations=[interfaces[2], interfaces[3]]
|
||||
)
|
||||
cable2.clean()
|
||||
cable2.save()
|
||||
|
||||
# Check for two complete paths in either direction
|
||||
paths = [
|
||||
self.assertPathExists(
|
||||
(interfaces[0], cable1, circuittermination1, circuittermination2, cable2, interfaces[2]),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[1], cable1, circuittermination1, circuittermination2, cable2, interfaces[3]),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[2], cable2, circuittermination2, circuittermination1, cable1, interfaces[0]),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
self.assertPathExists(
|
||||
(interfaces[3], cable2, circuittermination2, circuittermination1, cable1, interfaces[1]),
|
||||
is_complete=True,
|
||||
is_active=True,
|
||||
),
|
||||
]
|
||||
self.assertEqual(CablePath.objects.count(), 4)
|
||||
|
||||
for interface in interfaces:
|
||||
interface.refresh_from_db()
|
||||
self.assertPathIsSet(interfaces[0], paths[0])
|
||||
self.assertPathIsSet(interfaces[1], paths[1])
|
||||
self.assertPathIsSet(interfaces[2], paths[2])
|
||||
self.assertPathIsSet(interfaces[3], paths[3])
|
||||
|
||||
# Test SVG generation
|
||||
CableTraceSVG(interfaces[0]).render()
|
||||
|
||||
def test_217_interface_to_interface_via_rear_ports(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [RP3] [FP3] --C3-- [IF2]
|
||||
[FP2] [RP2] [RP4] [FP4]
|
||||
"""
|
||||
interfaces = [
|
||||
Interface.objects.create(device=self.device, name='Interface 1'),
|
||||
Interface.objects.create(device=self.device, name='Interface 2'),
|
||||
]
|
||||
rear_ports = [
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1),
|
||||
RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1),
|
||||
]
|
||||
front_ports = [
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
|
||||
),
|
||||
FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 4', rear_port=rear_ports[3], rear_port_position=1
|
||||
),
|
||||
]
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[0]],
|
||||
b_terminations=[front_ports[0], front_ports[1]]
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
a_terminations=[rear_ports[0], rear_ports[1]],
|
||||
b_terminations=[rear_ports[2], rear_ports[3]]
|
||||
)
|
||||
cable2.clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[1]],
|
||||
b_terminations=[front_ports[2], front_ports[3]]
|
||||
)
|
||||
cable3.clean()
|
||||
cable3.save()
|
||||
|
||||
# Check for one complete path in either direction
|
||||
paths = [
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[0], cable1, (front_ports[0], front_ports[1]), (rear_ports[0], rear_ports[1]), cable2,
|
||||
(rear_ports[2], rear_ports[3]), (front_ports[2], front_ports[3]), cable3, interfaces[1]
|
||||
),
|
||||
is_complete=True
|
||||
),
|
||||
self.assertPathExists(
|
||||
(
|
||||
interfaces[1], cable3, (front_ports[2], front_ports[3]), (rear_ports[2], rear_ports[3]), cable2,
|
||||
(rear_ports[0], rear_ports[1]), (front_ports[0], front_ports[1]), cable1, interfaces[0]
|
||||
),
|
||||
is_complete=True
|
||||
),
|
||||
]
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
|
||||
for interface in interfaces:
|
||||
interface.refresh_from_db()
|
||||
self.assertPathIsSet(interfaces[0], paths[0])
|
||||
self.assertPathIsSet(interfaces[1], paths[1])
|
||||
|
||||
# Test SVG generation
|
||||
CableTraceSVG(interfaces[0]).render()
|
||||
|
||||
# TODO: Revisit this test under FR #20564
|
||||
@skipIf(True, "Waiting for FR #20564")
|
||||
def test_223_single_path_via_multiple_pass_throughs_with_breakouts(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF3]
|
||||
[IF2] [FP2] [RP2] [IF4]
|
||||
"""
|
||||
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'),
|
||||
]
|
||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
|
||||
frontport1 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
|
||||
)
|
||||
frontport2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
|
||||
)
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[interfaces[0], interfaces[1]],
|
||||
b_terminations=[frontport1, frontport2]
|
||||
)
|
||||
cable1.clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
profile=CableProfileChoices.STRAIGHT_MULTI,
|
||||
a_terminations=[rearport1, rearport2],
|
||||
b_terminations=[interfaces[2], interfaces[3]]
|
||||
)
|
||||
cable2.clean()
|
||||
cable2.save()
|
||||
|
||||
for path in CablePath.objects.all():
|
||||
print(f'{path}: {path.path_objects}')
|
||||
|
||||
# Validate paths
|
||||
self.assertPathExists(
|
||||
(interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interfaces[1], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[3]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interfaces[2], cable2, [rearport1, rearport2], [frontport1, frontport2], cable1, interfaces[0]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertPathExists(
|
||||
(interfaces[3], cable2, [rearport1, rearport2], [frontport1, frontport2], cable1, interfaces[1]),
|
||||
is_complete=True,
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 4)
|
||||
88
netbox/dcim/tests/utils.py
Normal file
88
netbox/dcim/tests/utils.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import *
|
||||
from dcim.models import *
|
||||
from dcim.utils import object_to_path_node
|
||||
|
||||
__all__ = (
|
||||
'CablePathTestCase',
|
||||
)
|
||||
|
||||
|
||||
class CablePathTestCase(TestCase):
|
||||
"""
|
||||
Base class for test cases for cable paths.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device')
|
||||
role = DeviceRole.objects.create(name='Device Role', slug='device-role')
|
||||
provider = Provider.objects.create(name='Provider', slug='provider')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
|
||||
|
||||
# Create reusable test objects
|
||||
cls.site = Site.objects.create(name='Site', slug='site')
|
||||
cls.device = Device.objects.create(site=cls.site, device_type=device_type, role=role, name='Test Device')
|
||||
cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel')
|
||||
cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
|
||||
|
||||
def _get_cablepath(self, nodes, **kwargs):
|
||||
"""
|
||||
Return a given cable path
|
||||
|
||||
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
|
||||
|
||||
:return: The matching CablePath (if any)
|
||||
"""
|
||||
path = []
|
||||
for step in nodes:
|
||||
if type(step) in (list, tuple):
|
||||
path.append([object_to_path_node(node) for node in step])
|
||||
else:
|
||||
path.append([object_to_path_node(step)])
|
||||
return CablePath.objects.filter(path=path, **kwargs).first()
|
||||
|
||||
def assertPathExists(self, nodes, **kwargs):
|
||||
"""
|
||||
Assert that a CablePath from origin to destination with a specific intermediate path exists. Returns the
|
||||
first matching CablePath, if found.
|
||||
|
||||
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
|
||||
"""
|
||||
cablepath = self._get_cablepath(nodes, **kwargs)
|
||||
self.assertIsNotNone(cablepath, msg='CablePath not found')
|
||||
|
||||
return cablepath
|
||||
|
||||
def assertPathDoesNotExist(self, nodes, **kwargs):
|
||||
"""
|
||||
Assert that a specific CablePath does *not* exist.
|
||||
|
||||
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
|
||||
"""
|
||||
cablepath = self._get_cablepath(nodes, **kwargs)
|
||||
self.assertIsNone(cablepath, msg='Unexpected CablePath found')
|
||||
|
||||
def assertPathIsSet(self, origin, cablepath, msg=None):
|
||||
"""
|
||||
Assert that a specific CablePath instance is set as the path on the origin.
|
||||
|
||||
:param origin: The originating path endpoint
|
||||
:param cablepath: The CablePath instance originating from this endpoint
|
||||
:param msg: Custom failure message (optional)
|
||||
"""
|
||||
if msg is None:
|
||||
msg = f"Path #{cablepath.pk} not set on originating endpoint {origin}"
|
||||
self.assertEqual(origin._path_id, cablepath.pk, msg=msg)
|
||||
|
||||
def assertPathIsNotSet(self, origin, msg=None):
|
||||
"""
|
||||
Assert that a specific CablePath instance is set as the path on the origin.
|
||||
|
||||
:param origin: The originating path endpoint
|
||||
:param msg: Custom failure message (optional)
|
||||
"""
|
||||
if msg is None:
|
||||
msg = f"Path #{origin._path_id} set as origin on {origin}; should be None!"
|
||||
self.assertIsNone(origin._path_id, msg=msg)
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import router, transaction
|
||||
@@ -31,16 +33,21 @@ def path_node_to_object(repr):
|
||||
return ct.model_class().objects.filter(pk=object_id).first()
|
||||
|
||||
|
||||
def create_cablepath(terminations):
|
||||
def create_cablepaths(objects):
|
||||
"""
|
||||
Create CablePaths for all paths originating from the specified set of nodes.
|
||||
|
||||
:param terminations: Iterable of CableTermination objects
|
||||
:param objects: Iterable of cabled objects (e.g. Interfaces)
|
||||
"""
|
||||
from dcim.models import CablePath
|
||||
|
||||
cp = CablePath.from_origin(terminations)
|
||||
if cp:
|
||||
# Arrange objects by cable position. All objects with a null position are grouped together.
|
||||
origins = defaultdict(list)
|
||||
for obj in objects:
|
||||
origins[obj.cable_position].append(obj)
|
||||
|
||||
for position, objects in origins.items():
|
||||
if cp := CablePath.from_origin(objects):
|
||||
cp.save()
|
||||
|
||||
|
||||
@@ -56,7 +63,7 @@ def rebuild_paths(terminations):
|
||||
with transaction.atomic(using=router.db_for_write(CablePath)):
|
||||
for cp in cable_paths:
|
||||
cp.delete()
|
||||
create_cablepath(cp.origins)
|
||||
create_cablepaths(cp.origins)
|
||||
|
||||
|
||||
def update_interface_bridges(device, interface_templates, module=None):
|
||||
|
||||
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
4
netbox/project-static/dist/netbox.js.map
vendored
4
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -20,6 +20,11 @@ export function getPlugins(element: HTMLSelectElement): object {
|
||||
};
|
||||
}
|
||||
|
||||
// Enable drag-and-drop reordering of items on multi-select fields
|
||||
if (element.hasAttribute('multiple')) {
|
||||
plugins.drag_drop = {};
|
||||
}
|
||||
|
||||
return {
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Profile" %}</th>
|
||||
<td>{% badge object.get_profile_display %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<h2 class="col-9 offset-3">{% trans "Cable" %}</h2>
|
||||
</div>
|
||||
{% render_field form.status %}
|
||||
{% render_field form.profile %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.label %}
|
||||
{% render_field form.description %}
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.dispatch import receiver
|
||||
|
||||
from dcim.exceptions import UnsupportedCablePath
|
||||
from dcim.models import CablePath, Interface
|
||||
from dcim.utils import create_cablepath
|
||||
from dcim.utils import create_cablepaths
|
||||
from utilities.exceptions import AbortRequest
|
||||
from .models import WirelessLink
|
||||
|
||||
@@ -37,7 +37,7 @@ def update_connected_interfaces(instance, created, raw=False, **kwargs):
|
||||
if created:
|
||||
for interface in (instance.interface_a, instance.interface_b):
|
||||
try:
|
||||
create_cablepath([interface])
|
||||
create_cablepaths([interface])
|
||||
except UnsupportedCablePath as e:
|
||||
raise AbortRequest(e)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user