mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-27 15:47:46 -06:00
@@ -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,23 +727,31 @@ class CablePath(models.Model):
|
||||
|
||||
# Step 6: Determine the far-end terminations
|
||||
if isinstance(links[0], Cable):
|
||||
termination_type = ObjectType.objects.get_for_model(terminations[0])
|
||||
local_cable_terminations = CableTermination.objects.filter(
|
||||
termination_type=termination_type,
|
||||
termination_id__in=[t.pk for t in terminations]
|
||||
)
|
||||
# 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]
|
||||
|
||||
q_filter = Q()
|
||||
for lct in local_cable_terminations:
|
||||
cable_end = 'A' if lct.cable_end == 'B' else 'B'
|
||||
q_filter |= Q(cable=lct.cable, cable_end=cable_end)
|
||||
# Legacy (positionless) behavior
|
||||
else:
|
||||
termination_type = ObjectType.objects.get_for_model(terminations[0])
|
||||
local_cable_terminations = CableTermination.objects.filter(
|
||||
termination_type=termination_type,
|
||||
termination_id__in=[t.pk for t in terminations]
|
||||
)
|
||||
|
||||
# Make sure this filter has been populated; if not, we have probably been given invalid data
|
||||
if not q_filter:
|
||||
break
|
||||
q_filter = Q()
|
||||
for lct in local_cable_terminations:
|
||||
cable_end = 'A' if lct.cable_end == 'B' else 'B'
|
||||
q_filter |= Q(cable=lct.cable, cable_end=cable_end)
|
||||
|
||||
remote_cable_terminations = CableTermination.objects.filter(q_filter)
|
||||
remote_terminations = [ct.termination for ct in remote_cable_terminations]
|
||||
# Make sure this filter has been populated; if not, we have probably been given invalid data
|
||||
if not q_filter:
|
||||
break
|
||||
|
||||
remote_cable_terminations = CableTermination.objects.filter(q_filter)
|
||||
remote_terminations = [ct.termination for ct in remote_cable_terminations]
|
||||
else:
|
||||
# WirelessLink
|
||||
remote_terminations = [
|
||||
|
||||
@@ -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:
|
||||
raise ValidationError({
|
||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||
})
|
||||
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,17 +33,22 @@ 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:
|
||||
cp.save()
|
||||
# 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()
|
||||
|
||||
|
||||
def rebuild_paths(terminations):
|
||||
@@ -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