Compare commits

...

46 Commits

Author SHA1 Message Date
Jeremy Stretch
c6416fded5 Merge fa70430942 into afba5b2791 2025-11-26 15:06:24 -05:00
Jeremy Stretch
fa70430942 Enable defining port mappings when importing device/module types
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
2025-11-26 15:04:43 -05:00
Jeremy Stretch
9e367b8fc1 Update logic for handling split cable paths 2025-11-26 14:44:38 -05:00
Jeremy Stretch
715974cefc Update GraphQL types & filters 2025-11-26 14:34:15 -05:00
Jeremy Stretch
836ddbf49d Add FKs from PortMapping & PortTemplateMapping to their parent models 2025-11-26 14:15:26 -05:00
Jeremy Stretch
006407f7e4 get_related_models() should ignore models marked as private 2025-11-26 14:09:48 -05:00
Jeremy Stretch
e6b1f942cd Misc cleanup
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-26 12:20:40 -05:00
Jeremy Stretch
06447ac637 Optimize replication of port mappings from DeviceType 2025-11-26 11:48:50 -05:00
Jeremy Stretch
ca880218d9 Validate position count on FrontPort & FrontPortTemplate 2025-11-26 10:32:49 -05:00
Jeremy Stretch
7c2193685e Consolidate create() and update() logic into PortSerializer base class 2025-11-26 10:21:15 -05:00
Jeremy Stretch
b993ec978a Rename port assignments to port mappings 2025-11-26 09:56:42 -05:00
Jeremy Stretch
81858388de Merge branch 'feature' into 20564-port-mappings
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-25 16:25:07 -05:00
Jeremy Stretch
2e37e4919e Merge branch 'feature' into 20564-port-mappings 2025-11-25 14:54:47 -05:00
Jeremy Stretch
d2afab9662 Remove obsolete GraphQL filters
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
2025-11-21 16:13:22 -05:00
Jeremy Stretch
b538ff80d5 Clean up tests 2025-11-21 15:24:00 -05:00
Jeremy Stretch
62620101db UI cleanup for front/rear ports 2025-11-21 14:12:00 -05:00
Jeremy Stretch
85d4066501 Simplify nested port assignment representation 2025-11-21 13:41:25 -05:00
Jeremy Stretch
66bbfa7a88 Remove rear_ports M2M fields from FrontPort & FrontPortTemplate 2025-11-21 13:15:57 -05:00
Jeremy Stretch
bfff2d7658 Update API tests 2025-11-21 12:41:06 -05:00
Jeremy Stretch
5b8d80a371 Fix filterset tests 2025-11-21 10:06:43 -05:00
Jeremy Stretch
b9d57c74ca Update migrations 2025-11-21 08:35:25 -05:00
Jeremy Stretch
e71e4ef0ce Replicate front/rear port assignments from DeviceType
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-20 16:42:54 -05:00
Jeremy Stretch
1e0748e618 Refactor PortAssignment and PortAssignmentTemplate into PortAssignmentBase 2025-11-20 15:43:18 -05:00
Jeremy Stretch
f067122ccd Add PortAssignmentTemplate for device types 2025-11-20 15:32:11 -05:00
Jeremy Stretch
4f54b29f48 Default FrontPort.positions to 1, to match RearPort
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-20 08:34:59 -05:00
Jeremy Stretch
9dbb9bb51c Update cable path tests
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-19 16:20:44 -05:00
Jeremy Stretch
f49b88ad5e Permit FrontPort.positions to be null 2025-11-19 09:47:36 -05:00
Jeremy Stretch
5e8d57f231 Update path tracing logic (WIP)
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-18 16:53:53 -05:00
Jeremy Stretch
a7c3971a43 Fix FrontPortCreateForm 2025-11-18 11:21:29 -05:00
Jeremy Stretch
4790dbba96 Exclude occupied rear port & position pairs from list of choices 2025-11-18 10:56:05 -05:00
Jeremy Stretch
6a7027aebb Update FrontPort model form
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-18 10:41:47 -05:00
Jeremy Stretch
c09b0771b2 Add positions field on FrontPort; remove legacy fields 2025-11-17 14:59:56 -05:00
Jeremy Stretch
2b420bde3a Introduce PortAssignment M2M mapping 2025-11-17 14:22:16 -05:00
Jeremy Stretch
b235c5c99f Rebase migrations
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
2025-11-17 12:42:40 -05:00
Jeremy Stretch
5a7f86a6f5 Clean up cable profiles 2025-11-17 12:39:17 -05:00
Jeremy Stretch
576c0db77d Document profile field 2025-11-17 12:39:17 -05:00
Jeremy Stretch
aa7eedac42 Remove many-to-one profiles 2025-11-17 12:39:17 -05:00
Jeremy Stretch
a75dee745e Enable drag-and-drop of items within multiselect fields 2025-11-17 12:39:17 -05:00
Jeremy Stretch
24c6653356 Add profile for 4x4 MPO8 shuffle cable 2025-11-17 12:39:17 -05:00
Jeremy Stretch
fb2ea37443 Simplify A/B side popping logic 2025-11-17 12:39:17 -05:00
Jeremy Stretch
2fe5323dd2 Add topology tests for cable profiles 2025-11-17 12:39:17 -05:00
Jeremy Stretch
fe95d89db3 Fix test 2025-11-17 12:39:17 -05:00
Jeremy Stretch
7edea73f85 Add initial cable path tests for profiles 2025-11-17 12:39:17 -05:00
Jeremy Stretch
481811e487 Misc cleanup 2025-11-17 12:39:17 -05:00
Jeremy Stretch
a20ac40b48 Add missing filters for cable_position 2025-11-17 12:39:17 -05:00
Jeremy Stretch
0901694b2b Initial work on FR #20788 (cable profiles) 2025-11-17 12:39:17 -05:00
35 changed files with 2138 additions and 900 deletions

View File

@@ -2,10 +2,12 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate
from utilities.api import get_serializer_for_model
__all__ = (
'ConnectedEndpointsSerializer',
'PortSerializer',
)
@@ -35,3 +37,53 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
@extend_schema_field(serializers.BooleanField)
def get_connected_endpoints_reachable(self, obj):
return obj._path and obj._path.is_complete and obj._path.is_active
class PortSerializer(serializers.ModelSerializer):
"""
Base serializer for front & rear port and port templates.
"""
@property
def _mapper(self):
"""
Return the model and ForeignKey field name used to track port mappings for this model.
"""
if self.Meta.model is FrontPort:
return PortMapping, 'front_port'
if self.Meta.model is RearPort:
return PortMapping, 'rear_port'
if self.Meta.model is FrontPortTemplate:
return PortTemplateMapping, 'front_port'
if self.Meta.model is RearPortTemplate:
return PortTemplateMapping, 'rear_port'
raise ValueError(f"Could not determine mapping details for {self.__class__}")
def create(self, validated_data):
mappings = validated_data.pop('mappings', [])
instance = super().create(validated_data)
# Create port mappings
mapping_model, fk_name = self._mapper
for attrs in mappings:
mapping_model.objects.create(**{
fk_name: instance,
**attrs,
})
return instance
def update(self, instance, validated_data):
mappings = validated_data.pop('mappings', None)
instance = super().update(instance, validated_data)
if mappings is not None:
# Update port mappings
mapping_model, fk_name = self._mapper
mapping_model.objects.filter(**{fk_name: instance}).delete()
for attrs in mappings:
mapping_model.objects.create(**{
fk_name: instance,
**attrs,
})
return instance

View File

@@ -5,21 +5,21 @@ from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort, VirtualDeviceContext,
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortMapping,
PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
)
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from netbox.api.serializers import NetBoxModelSerializer
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
from wireless.choices import *
from wireless.models import WirelessLAN
from .base import ConnectedEndpointsSerializer
from .base import ConnectedEndpointsSerializer, PortSerializer
from .cables import CabledObjectSerializer
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
from .manufacturers import ManufacturerSerializer
@@ -294,7 +294,20 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
return super().validate(data)
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class RearPortMappingSerializer(serializers.ModelSerializer):
position = serializers.IntegerField(
source='rear_port_position'
)
front_port = serializers.PrimaryKeyRelatedField(
queryset=FrontPort.objects.all(),
)
class Meta:
model = PortMapping
fields = ('position', 'front_port', 'front_port_position')
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
@@ -303,28 +316,36 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
front_ports = RearPortMappingSerializer(
source='mappings',
many=True,
required=False,
)
class Meta:
model = RearPort
fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
'custom_fields', 'created', 'last_updated', '_occupied',
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class FrontPortRearPortSerializer(WritableNestedSerializer):
"""
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
"""
class FrontPortMappingSerializer(serializers.ModelSerializer):
position = serializers.IntegerField(
source='front_port_position'
)
rear_port = serializers.PrimaryKeyRelatedField(
queryset=RearPort.objects.all(),
)
class Meta:
model = RearPort
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
model = PortMapping
fields = ('position', 'rear_port', 'rear_port_position')
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
@@ -333,14 +354,18 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer()
rear_ports = FrontPortMappingSerializer(
source='mappings',
many=True,
required=False,
)
class Meta:
model = FrontPort
fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')

View File

@@ -5,12 +5,14 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
InventoryItemTemplate, ModuleBayTemplate, PortTemplateMapping, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
)
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
from wireless.choices import *
from .base import PortSerializer
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
from .manufacturers import ManufacturerSerializer
from .nested import NestedInterfaceTemplateSerializer
@@ -205,7 +207,20 @@ class InterfaceTemplateSerializer(ComponentTemplateSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class RearPortTemplateSerializer(ComponentTemplateSerializer):
class RearPortTemplateMappingSerializer(serializers.ModelSerializer):
position = serializers.IntegerField(
source='rear_port_position'
)
front_port = serializers.PrimaryKeyRelatedField(
queryset=FrontPortTemplate.objects.all(),
)
class Meta:
model = PortTemplateMapping
fields = ('position', 'front_port', 'front_port_position')
class RearPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
device_type = DeviceTypeSerializer(
required=False,
nested=True,
@@ -219,17 +234,35 @@ class RearPortTemplateSerializer(ComponentTemplateSerializer):
default=None
)
type = ChoiceField(choices=PortTypeChoices)
front_ports = RearPortTemplateMappingSerializer(
source='mappings',
many=True,
required=False,
)
class Meta:
model = RearPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
'positions', 'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
'front_ports', 'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class FrontPortTemplateSerializer(ComponentTemplateSerializer):
class FrontPortTemplateMappingSerializer(serializers.ModelSerializer):
position = serializers.IntegerField(
source='front_port_position'
)
rear_port = serializers.PrimaryKeyRelatedField(
queryset=RearPortTemplate.objects.all(),
)
class Meta:
model = PortTemplateMapping
fields = ('position', 'rear_port', 'rear_port_position')
class FrontPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -243,13 +276,17 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer):
default=None
)
type = ChoiceField(choices=PortTypeChoices)
rear_port = RearPortTemplateSerializer(nested=True)
rear_ports = FrontPortTemplateMappingSerializer(
source='mappings',
many=True,
required=False,
)
class Meta:
model = FrontPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
'rear_ports', 'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@@ -32,8 +32,8 @@ CABLE_POSITION_MAX = 1024
# RearPorts
#
REARPORT_POSITIONS_MIN = 1
REARPORT_POSITIONS_MAX = 1024
PORT_POSITION_MIN = 1
PORT_POSITION_MAX = 1024
#

View File

@@ -885,12 +885,15 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
null_value=None
)
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
field_name='mappings__rear_port',
queryset=RearPort.objects.all(),
to_field_name='rear_port',
label=_('Rear port (ID)'),
)
class Meta:
model = FrontPortTemplate
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -898,6 +901,12 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
choices=PortTypeChoices,
null_value=None
)
front_port_id = django_filters.ModelMultipleChoiceFilter(
field_name='mappings__front_port',
queryset=FrontPort.objects.all(),
to_field_name='front_port',
label=_('Front port (ID)'),
)
class Meta:
model = RearPortTemplate
@@ -2102,13 +2111,16 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
null_value=None
)
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
field_name='mappings__rear_port',
queryset=RearPort.objects.all(),
to_field_name='rear_port',
label=_('Rear port (ID)'),
)
class Meta:
model = FrontPort
fields = (
'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end',
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
'cable_position',
)
@@ -2118,6 +2130,12 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
choices=PortTypeChoices,
null_value=None
)
front_port_id = django_filters.ModelMultipleChoiceFilter(
field_name='mappings__front_port',
queryset=FrontPort.objects.all(),
to_field_name='front_port',
label=_('Front port (ID)'),
)
class Meta:
model = RearPort

View File

@@ -1075,12 +1075,6 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
queryset=Device.objects.all(),
to_field_name='name'
)
rear_port = CSVModelChoiceField(
label=_('Rear port'),
queryset=RearPort.objects.all(),
to_field_name='name',
help_text=_('Corresponding rear port')
)
type = CSVChoiceField(
label=_('Type'),
choices=PortTypeChoices,
@@ -1090,32 +1084,9 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
class Meta:
model = FrontPort
fields = (
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
'description', 'owner', 'tags'
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'owner', 'tags'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit RearPort choices to those belonging to this device (or VC master)
if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['rear_port'].queryset = RearPort.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['rear_port'].queryset = RearPort.objects.none()
class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
device = CSVModelChoiceField(

View File

@@ -4,7 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.constants import LOCATION_SCOPE_TYPES
from dcim.models import Site
from dcim.models import PortMapping, PortTemplateMapping, Site
from utilities.forms import get_field_value
from utilities.forms.fields import (
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
@@ -13,6 +13,7 @@ from utilities.templatetags.builtins.filters import bettertitle
from utilities.forms.widgets import HTMXSelect
__all__ = (
'FrontPortFormMixin',
'ScopedBulkEditForm',
'ScopedForm',
'ScopedImportForm',
@@ -128,3 +129,56 @@ class ScopedImportForm(forms.Form):
"Please select a {scope_type}."
).format(scope_type=scope_type.model_class()._meta.model_name)
})
class FrontPortFormMixin(forms.Form):
rear_ports = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
widget=forms.SelectMultiple(attrs={'size': 8})
)
port_mapping_model = PortMapping
parent_field = 'device'
def clean(self):
super().clean()
# Count of selected rear port & position pairs much match the assigned number of positions
if len(self.cleaned_data['rear_ports']) != self.cleaned_data['positions']:
raise forms.ValidationError({
'rear_ports': _(
"The number of rear port/position pairs selected must match the number of positions assigned."
)
})
def _save_m2m(self):
super()._save_m2m()
# TODO: Can this be made more efficient?
# Delete existing rear port mappings
self.port_mapping_model.objects.filter(front_port_id=self.instance.pk).delete()
# Create new rear port mappings
mappings = []
if self.port_mapping_model is PortTemplateMapping:
params = {
'device_type_id': self.instance.device_type_id,
'module_type_id': self.instance.module_type_id,
}
else:
params = {
'device_id': self.instance.device_id,
}
for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
rear_port_id, rear_port_position = rp_position.split(':')
mappings.append(
self.port_mapping_model(**{
**params,
'front_port_id': self.instance.pk,
'front_port_position': i,
'rear_port_id': rear_port_id,
'rear_port_position': rear_port_position,
})
)
self.port_mapping_model.objects.bulk_create(mappings)

View File

@@ -6,6 +6,7 @@ from timezone_field import TimeZoneFormField
from dcim.choices import *
from dcim.constants import *
from dcim.forms.mixins import FrontPortFormMixin
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.choices import VLANQinQRoleChoices
@@ -1112,34 +1113,66 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
]
class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type',
'module_type_id': '$module_type',
}
)
class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'name', 'label', 'type', 'positions', 'rear_ports', 'description',
),
)
# Override FrontPortFormMixin attrs
port_mapping_model = PortTemplateMapping
parent_field = 'device_type'
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
device_type = DeviceType.objects.get(pk=device_type_id)
else:
return
# Populate rear port choices
self.fields['rear_ports'].choices = self._get_rear_port_choices(device_type, self.instance)
# Set initial rear port mappings
if self.instance.pk:
self.initial['rear_ports'] = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
]
def _get_rear_port_choices(self, device_type, front_port):
"""
Return a list of choices representing each available rear port & position pair on the device type, excluding
those assigned to the specified instance.
"""
occupied_rear_port_positions = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in device_type.port_mappings.exclude(front_port=front_port.pk)
]
choices = []
for rear_port in RearPortTemplate.objects.filter(device_type=device_type):
for i in range(1, rear_port.positions + 1):
pair_id = f'{rear_port.pk}:{i}'
if pair_id not in occupied_rear_port_positions:
pair_label = f'{rear_port.name}:{i}'
choices.append(
(pair_id, pair_label)
)
return choices
class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
@@ -1578,17 +1611,10 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
}
class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
query_params={
'device_id': '$device',
}
)
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
'description', 'tags',
),
)
@@ -1596,10 +1622,49 @@ class FrontPortForm(ModularDeviceComponentForm):
class Meta:
model = FrontPort
fields = [
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'description', 'owner', 'tags',
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
'tags',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if device_id := self.data.get('device') or self.initial.get('device'):
device = Device.objects.get(pk=device_id)
else:
return
# Populate rear port choices
self.fields['rear_ports'].choices = self._get_rear_port_choices(device, self.instance)
# Set initial rear port mappings
if self.instance.pk:
self.initial['rear_ports'] = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
]
def _get_rear_port_choices(self, device, front_port):
"""
Return a list of choices representing each available rear port & position pair on the device, excluding those
assigned to the specified instance.
"""
occupied_rear_port_positions = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in device.port_mappings.exclude(front_port=front_port.pk)
]
choices = []
for rear_port in RearPort.objects.filter(device=device):
for i in range(1, rear_port.positions + 1):
pair_id = f'{rear_port.pk}:{i}'
if pair_id not in occupied_rear_port_positions:
pair_label = f'{rear_port.name}:{i}'
choices.append(
(pair_id, pair_label)
)
return choices
class RearPortForm(ModularDeviceComponentForm):
fieldsets = (

View File

@@ -109,69 +109,36 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
rear_port = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
help_text=_('Select one rear port assignment for each front port being created.'),
widget=forms.SelectMultiple(attrs={'size': 6})
)
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
# Override fieldsets from FrontPortTemplateForm
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'description',
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
),
)
class Meta(model_forms.FrontPortTemplateForm.Meta):
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO: This needs better validation
if 'device_type' in self.initial or self.data.get('device_type'):
parent = DeviceType.objects.get(
pk=self.initial.get('device_type') or self.data.get('device_type')
)
elif 'module_type' in self.initial or self.data.get('module_type'):
parent = ModuleType.objects.get(
pk=self.initial.get('module_type') or self.data.get('module_type')
)
else:
return
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position)
for front_port in parent.frontporttemplates.all()
]
# Populate rear port choices
choices = []
rear_ports = parent.rearporttemplates.all()
for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions:
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port'].choices = choices
class Meta:
model = FrontPortTemplate
fields = (
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
)
def clean(self):
super().clean()
# TODO
# super(ComponentCreateForm, self).clean()
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
# positions
positions = self.cleaned_data['positions']
frontport_count = len(self.cleaned_data['name'])
rearport_count = len(self.cleaned_data['rear_port'])
if frontport_count != rearport_count:
rearport_count = len(self.cleaned_data['rear_ports'])
if frontport_count * positions != rearport_count:
raise forms.ValidationError({
'rear_port': _(
'rear_ports': _(
"The number of front port templates to be created ({frontport_count}) must match the selected "
"number of rear port positions ({rearport_count})."
).format(
@@ -181,13 +148,11 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
})
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
positions = self.cleaned_data['positions']
offset = positions * iteration
return {
'rear_port': int(rear_port),
'rear_port_position': int(position),
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
}
@@ -269,58 +234,31 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
}
)
)
rear_port = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
help_text=_('Select one rear port assignment for each front port being created.'),
widget=forms.SelectMultiple(attrs={'size': 6})
)
# Override fieldsets from FrontPortForm to omit rear_port_position
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
'description', 'tags',
),
)
class Meta(model_forms.FrontPortForm.Meta):
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if device_id := self.data.get('device') or self.initial.get('device'):
device = Device.objects.get(pk=device_id)
else:
return
# Determine which rear port positions are occupied. These will be excluded from the list of available
# mappings.
occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position)
for front_port in device.frontports.all()
class Meta:
model = FrontPort
fields = [
'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
]
# Populate rear port choices
choices = []
rear_ports = RearPort.objects.filter(device=device)
for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions:
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_port'].choices = choices
def clean(self):
super().clean()
super(NetBoxModelForm, self).clean()
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
# Check that the number of FrontPorts to be created matches the selected number of RearPorts
positions = self.cleaned_data['positions']
frontport_count = len(self.cleaned_data['name'])
rearport_count = len(self.cleaned_data['rear_port'])
if frontport_count != rearport_count:
rearport_count = len(self.cleaned_data['rear_ports'])
if frontport_count * positions != rearport_count:
raise forms.ValidationError({
'rear_port': _(
'rear_ports': _(
"The number of front ports to be created ({frontport_count}) must match the selected number of "
"rear port positions ({rearport_count})."
).format(
@@ -330,13 +268,10 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
})
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
positions = self.cleaned_data['positions']
offset = positions * iteration
return {
'rear_port': int(rear_port),
'rear_port_position': int(position),
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
}

View File

@@ -13,6 +13,7 @@ __all__ = (
'InterfaceTemplateImportForm',
'InventoryItemTemplateImportForm',
'ModuleBayTemplateImportForm',
'PortTemplateMappingImportForm',
'PowerOutletTemplateImportForm',
'PowerPortTemplateImportForm',
'RearPortTemplateImportForm',
@@ -113,31 +114,11 @@ class FrontPortTemplateImportForm(forms.ModelForm):
label=_('Type'),
choices=PortTypeChoices.CHOICES
)
rear_port = forms.ModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
to_field_name='name'
)
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(module_type=module_type)
return module_type
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label',
'description',
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
]
@@ -154,6 +135,25 @@ class RearPortTemplateImportForm(forms.ModelForm):
]
class PortTemplateMappingImportForm(forms.ModelForm):
front_port = forms.ModelChoiceField(
label=_('Front port'),
queryset=FrontPortTemplate.objects.all(),
to_field_name='name',
)
rear_port = forms.ModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
to_field_name='name',
)
class Meta:
model = PortTemplateMapping
fields = [
'front_port', 'front_port_position', 'rear_port', 'rear_port_position',
]
class ModuleBayTemplateImportForm(forms.ModelForm):
class Meta:

View File

@@ -6,7 +6,7 @@ import strawberry_django
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup
from core.graphql.filter_mixins import ChangeLogFilterMixin
from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin
from dcim import models
from dcim.constants import *
from dcim.graphql.enums import InterfaceKindEnum
@@ -75,6 +75,8 @@ __all__ = (
'ModuleTypeFilter',
'ModuleTypeProfileFilter',
'PlatformFilter',
'PortMappingFilter',
'PortTemplateMappingFilter',
'PowerFeedFilter',
'PowerOutletFilter',
'PowerOutletTemplateFilter',
@@ -409,13 +411,6 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_port_id: ID | None = strawberry_django.filter_field()
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
@@ -426,13 +421,37 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter_type(models.PortMapping, lookups=True)
class PortMappingFilter(BaseObjectTypeFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
front_port: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.PortTemplateMapping, lookups=True)
class PortTemplateMappingFilter(BaseObjectTypeFilterMixin):
device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
front_port: Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
rear_port_id: ID | None = strawberry_django.filter_field()
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.MACAddress, lookups=True)

View File

@@ -385,7 +385,8 @@ class DeviceTypeType(PrimaryObjectType):
)
class FrontPortType(ModularComponentType, CabledObjectMixin):
color: str
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -396,7 +397,8 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
)
class FrontPortTemplateType(ModularComponentTemplateType):
color: str
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -636,6 +638,28 @@ class PlatformType(NestedGroupObjectType):
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
models.PortMapping,
fields='__all__',
filters=PortMappingFilter,
pagination=True
)
class PortMappingType(ModularComponentTemplateType):
front_port: Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
models.PortTemplateMapping,
fields='__all__',
filters=PortTemplateMappingFilter,
pagination=True
)
class PortMappingTemplateType(ModularComponentTemplateType):
front_port: Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
models.PowerFeed,
exclude=['_path'],
@@ -768,7 +792,7 @@ class RackRoleType(OrganizationalObjectType):
class RearPortType(ModularComponentType, CabledObjectMixin):
color: str
frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -780,7 +804,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
class RearPortTemplateType(ModularComponentTemplateType):
color: str
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(

View File

@@ -0,0 +1,219 @@
import django.core.validators
import django.db.models.deletion
from django.db import migrations
from django.db import models
from itertools import islice
def chunked(iterable, size):
"""
Yield successive chunks of a given size from an iterator.
"""
iterator = iter(iterable)
while chunk := list(islice(iterator, size)):
yield chunk
def populate_port_template_mappings(apps, schema_editor):
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
PortTemplateMapping = apps.get_model('dcim', 'PortTemplateMapping')
front_ports = FrontPortTemplate.objects.iterator(chunk_size=1000)
def generate_copies():
for front_port in front_ports:
yield PortTemplateMapping(
device_type_id=front_port.device_type_id,
module_type_id=front_port.module_type_id,
front_port_id=front_port.pk,
front_port_position=1,
rear_port_id=front_port.rear_port_id,
rear_port_position=front_port.rear_port_position,
)
# Bulk insert in streaming batches
for chunk in chunked(generate_copies(), 1000):
PortTemplateMapping.objects.bulk_create(chunk, batch_size=1000)
def populate_port_mappings(apps, schema_editor):
FrontPort = apps.get_model('dcim', 'FrontPort')
PortMapping = apps.get_model('dcim', 'PortMapping')
front_ports = FrontPort.objects.iterator(chunk_size=1000)
def generate_copies():
for front_port in front_ports:
yield PortMapping(
device_id=front_port.device_id,
front_port_id=front_port.pk,
front_port_position=1,
rear_port_id=front_port.rear_port_id,
rear_port_position=front_port.rear_port_position,
)
# Bulk insert in streaming batches
for chunk in chunked(generate_copies(), 1000):
PortMapping.objects.bulk_create(chunk, batch_size=1000)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0221_cable_position'),
]
operations = [
# Create PortTemplateMapping model (for DeviceTypes)
migrations.CreateModel(
name='PortTemplateMapping',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
(
'front_port_position',
models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
)
),
(
'rear_port_position',
models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
)
),
(
'device_type',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.devicetype',
related_name='port_mappings',
blank=True,
null=True
)
),
(
'module_type',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.moduletype',
related_name='port_mappings',
blank=True,
null=True
)
),
(
'front_port',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.frontporttemplate',
related_name='mappings'
)
),
(
'rear_port',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.rearporttemplate',
related_name='mappings'
)
),
],
),
migrations.AddConstraint(
model_name='porttemplatemapping',
constraint=models.UniqueConstraint(
fields=('front_port', 'front_port_position'),
name='dcim_porttemplatemapping_unique_front_port_position'
),
),
migrations.AddConstraint(
model_name='porttemplatemapping',
constraint=models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='dcim_porttemplatemapping_unique_rear_port_position'
),
),
# Create PortMapping model (for Devices)
migrations.CreateModel(
name='PortMapping',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
(
'front_port_position',
models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
),
),
(
'rear_port_position',
models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024),
]
),
),
(
'device',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.device',
related_name='port_mappings'
)
),
(
'front_port',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.frontport',
related_name='mappings'
)
),
(
'rear_port',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='dcim.rearport',
related_name='mappings'
)
),
],
),
migrations.AddConstraint(
model_name='portmapping',
constraint=models.UniqueConstraint(
fields=('front_port', 'front_port_position'),
name='dcim_portmapping_unique_front_port_position'
),
),
migrations.AddConstraint(
model_name='portmapping',
constraint=models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='dcim_portmapping_unique_rear_port_position'
),
),
# Data migration
migrations.RunPython(
code=populate_port_template_mappings,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=populate_port_mappings,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,65 @@
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0222_port_mappings'),
]
operations = [
# Remove rear_port & rear_port_position from FrontPortTemplate
migrations.RemoveConstraint(
model_name='frontporttemplate',
name='dcim_frontporttemplate_unique_rear_port_position',
),
migrations.RemoveField(
model_name='frontporttemplate',
name='rear_port',
),
migrations.RemoveField(
model_name='frontporttemplate',
name='rear_port_position',
),
# Add positions on FrontPortTemplate
migrations.AddField(
model_name='frontporttemplate',
name='positions',
field=models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
),
),
# Remove rear_port & rear_port_position from FrontPort
migrations.RemoveConstraint(
model_name='frontport',
name='dcim_frontport_unique_rear_port_position',
),
migrations.RemoveField(
model_name='frontport',
name='rear_port',
),
migrations.RemoveField(
model_name='frontport',
name='rear_port_position',
),
# Add positions on FrontPort
migrations.AddField(
model_name='frontport',
name='positions',
field=models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
),
),
]

View File

@@ -0,0 +1,61 @@
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.constants import PORT_POSITION_MAX, PORT_POSITION_MIN
__all__ = (
'PortMappingBase',
)
class PortMappingBase(models.Model):
"""
Base class for PortMapping and PortTemplateMapping
"""
front_port_position = models.PositiveSmallIntegerField(
default=1,
validators=(
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX),
),
)
rear_port_position = models.PositiveSmallIntegerField(
default=1,
validators=(
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX),
),
)
_netbox_private = True
class Meta:
abstract = True
constraints = (
models.UniqueConstraint(
fields=('front_port', 'front_port_position'),
name='%(app_label)s_%(class)s_unique_front_port_position'
),
models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='%(app_label)s_%(class)s_unique_rear_port_position'
),
)
def clean(self):
super().clean()
# Validate rear port position
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": _(
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
"positions."
).format(
rear_port_position=self.rear_port_position,
name=self.rear_port.name,
positions=self.rear_port.positions
)
})

View File

@@ -22,7 +22,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, PathEndpoint, RearPort
from .device_components import FrontPort, PathEndpoint, PortMapping, RearPort
__all__ = (
'Cable',
@@ -666,7 +666,14 @@ class CablePath(models.Model):
is_active = True
is_split = False
DEBUG = False
segment = 0
while terminations:
segment += 1
if DEBUG:
print(f'[#{segment}] Position stack: {position_stack}')
print(f'[#{segment}] Local terminations: {terminations}')
# Terminations must all be of the same type
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
@@ -697,7 +704,11 @@ class CablePath(models.Model):
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]
links = list(dict.fromkeys(
termination.link for termination in terminations if termination.link is not None
))
if DEBUG:
print(f'[#{segment}] Links: {links}')
if len(links) == 0:
if len(path) == 1:
# If this is the start of the path and no link exists, return None
@@ -760,6 +771,8 @@ class CablePath(models.Model):
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
]
if DEBUG:
print(f'[#{segment}] Remote terminations: {remote_terminations}')
# Remote Terminations must all be of the same type, otherwise return a split path
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
is_complete = False
@@ -777,58 +790,45 @@ class CablePath(models.Model):
if isinstance(remote_terminations[0], FrontPort):
# Follow FrontPorts to their corresponding RearPorts
rear_ports = RearPort.objects.filter(
pk__in=[t.rear_port_id for t in remote_terminations]
)
if len(rear_ports) > 1 or rear_ports[0].positions > 1:
position_stack.append([fp.rear_port_position for fp in remote_terminations])
terminations = rear_ports
elif isinstance(remote_terminations[0], RearPort):
if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
front_ports = FrontPort.objects.filter(
rear_port_id__in=[rp.pk for rp in remote_terminations],
rear_port_position=1
)
# Obtain the individual front ports based on the termination and all positions
elif len(remote_terminations) > 1 and position_stack:
if remote_terminations[0].positions > 1 and position_stack:
positions = position_stack.pop()
# Ensure we have a number of positions equal to the amount of remote terminations
if len(remote_terminations) != len(positions):
raise UnsupportedCablePath(
_("All positions counts within the path on opposite ends of links must match")
)
# Get our front ports
q_filter = Q()
for rt in remote_terminations:
position = positions.pop()
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
if q_filter is Q():
raise UnsupportedCablePath(_("Remote termination position filter is missing"))
front_ports = FrontPort.objects.filter(q_filter)
# Obtain the individual front ports based on the termination and position
elif position_stack:
front_ports = FrontPort.objects.filter(
rear_port_id=remote_terminations[0].pk,
rear_port_position__in=position_stack.pop()
)
# If all rear ports have a single position, we can just get the front ports
elif all([rp.positions == 1 for rp in remote_terminations]):
front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
if len(front_ports) != len(remote_terminations):
# Some rear ports does not have a front port
is_split = True
break
else:
# No position indicated: path has split, so we stop at the RearPorts
q_filter |= Q(front_port=rt, front_port_position__in=positions)
port_mappings = PortMapping.objects.filter(q_filter)
elif remote_terminations[0].positions > 1:
is_split = True
break
else:
port_mappings = PortMapping.objects.filter(front_port__in=remote_terminations)
if not port_mappings:
break
terminations = front_ports
# Compile the list of RearPorts without duplication or altering their ordering
terminations = list(dict.fromkeys(mapping.rear_port for mapping in port_mappings))
if any(t.positions > 1 for t in terminations):
position_stack.append([mapping.rear_port_position for mapping in port_mappings])
elif isinstance(remote_terminations[0], RearPort):
# Follow RearPorts to their corresponding FrontPorts
if remote_terminations[0].positions > 1 and position_stack:
positions = position_stack.pop()
q_filter = Q()
for rt in remote_terminations:
q_filter |= Q(rear_port=rt, rear_port_position__in=positions)
port_mappings = PortMapping.objects.filter(q_filter)
elif remote_terminations[0].positions > 1:
is_split = True
break
else:
port_mappings = PortMapping.objects.filter(rear_port__in=remote_terminations)
if not port_mappings:
break
# Compile the list of FrontPorts without duplication or altering their ordering
terminations = list(dict.fromkeys(mapping.front_port for mapping in port_mappings))
if any(t.positions > 1 for t in terminations):
position_stack.append([mapping.front_port_position for mapping in port_mappings])
elif isinstance(remote_terminations[0], CircuitTermination):
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
@@ -954,16 +954,23 @@ class CablePath(models.Model):
# RearPort splitting to multiple FrontPorts with no stack position
if type(nodes[0]) is RearPort:
return FrontPort.objects.filter(rear_port__in=nodes)
return [
mapping.front_port for mapping in
PortMapping.objects.filter(rear_port__in=nodes).prefetch_related('front_port')
]
# Cable terminating to multiple FrontPorts mapped to different
# RearPorts connected to different cables
elif type(nodes[0]) is FrontPort:
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
if type(nodes[0]) is FrontPort:
return [
mapping.rear_port for mapping in
PortMapping.objects.filter(front_port__in=nodes).prefetch_related('rear_port')
]
# Cable terminating to multiple CircuitTerminations
elif type(nodes[0]) is CircuitTermination:
if type(nodes[0]) is CircuitTermination:
return [
ct.get_peer_termination() for ct in nodes
]
return []
def get_asymmetric_nodes(self):
"""

View File

@@ -7,6 +7,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
from dcim.constants import *
from dcim.models.base import PortMappingBase
from dcim.models.mixins import InterfaceValidationMixin
from netbox.models import ChangeLoggedModel
from utilities.fields import ColorField, NaturalOrderingField
@@ -28,6 +29,7 @@ __all__ = (
'InterfaceTemplate',
'InventoryItemTemplate',
'ModuleBayTemplate',
'PortTemplateMapping',
'PowerOutletTemplate',
'PowerPortTemplate',
'RearPortTemplate',
@@ -518,6 +520,53 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
}
class PortTemplateMapping(PortMappingBase):
"""
Maps a FrontPortTemplate & position to a RearPortTemplate & position.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='port_mappings',
blank=True,
null=True,
)
module_type = models.ForeignKey(
to='dcim.ModuleType',
on_delete=models.CASCADE,
related_name='port_mappings',
blank=True,
null=True,
)
front_port = models.ForeignKey(
to='dcim.FrontPortTemplate',
on_delete=models.CASCADE,
related_name='mappings',
)
rear_port = models.ForeignKey(
to='dcim.RearPortTemplate',
on_delete=models.CASCADE,
related_name='mappings',
)
def clean(self):
super().clean()
# Validate rear port assignment
if self.front_port.device_type_id != self.rear_port.device_type_id:
raise ValidationError({
"rear_port": _("Rear port ({rear_port}) must belong to the same device type").format(
rear_port=self.rear_port
)
})
def save(self, *args, **kwargs):
# Associate the mapping with the parent DeviceType/ModuleType
self.device_type = self.front_port.device_type
self.module_type = self.front_port.module_type
super().save(*args, **kwargs)
class FrontPortTemplate(ModularComponentTemplateModel):
"""
Template for a pass-through port on the front of a new Device.
@@ -531,18 +580,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
verbose_name=_('color'),
blank=True
)
rear_port = models.ForeignKey(
to='dcim.RearPortTemplate',
on_delete=models.CASCADE,
related_name='frontport_templates'
)
rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
positions = models.PositiveSmallIntegerField(
verbose_name=_('positions'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
MaxValueValidator(REARPORT_POSITIONS_MAX)
]
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX)
],
)
component_model = FrontPort
@@ -557,10 +601,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
fields=('module_type', 'name'),
name='%(app_label)s_%(class)s_unique_module_type_name'
),
models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='%(app_label)s_%(class)s_unique_rear_port_position'
),
)
verbose_name = _('front port template')
verbose_name_plural = _('front port templates')
@@ -568,40 +608,23 @@ class FrontPortTemplate(ModularComponentTemplateModel):
def clean(self):
super().clean()
try:
# Validate rear port assignment
if self.rear_port.device_type != self.device_type:
raise ValidationError(
_("Rear port ({name}) must belong to the same device type").format(name=self.rear_port)
)
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError(
_("Invalid rear port position ({position}); rear port {name} has only {count} positions").format(
position=self.rear_port_position,
name=self.rear_port.name,
count=self.rear_port.positions
)
)
except RearPortTemplate.DoesNotExist:
pass
# Check that positions is greater than or equal to the number of associated RearPortTemplates
if not self._state.adding:
mapping_count = self.mappings.count()
if self.positions < mapping_count:
raise ValidationError({
"positions": _(
"The number of positions cannot be less than the number of mapped rear port templates ({count})"
).format(count=mapping_count)
})
def instantiate(self, **kwargs):
if self.rear_port:
rear_port_name = self.rear_port.resolve_name(kwargs.get('module'))
rear_port = RearPort.objects.get(name=rear_port_name, **kwargs)
else:
rear_port = None
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
type=self.type,
color=self.color,
rear_port=rear_port,
rear_port_position=self.rear_port_position,
positions=self.positions,
**kwargs
)
instantiate.do_not_call_in_templates = True
@@ -611,8 +634,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
'name': self.name,
'type': self.type,
'color': self.color,
'rear_port': self.rear_port.name,
'rear_port_position': self.rear_port_position,
'positions': self.positions,
'label': self.label,
'description': self.description,
}
@@ -635,9 +657,9 @@ class RearPortTemplate(ModularComponentTemplateModel):
verbose_name=_('positions'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
MaxValueValidator(REARPORT_POSITIONS_MAX)
]
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX)
],
)
component_model = RearPort
@@ -646,6 +668,20 @@ class RearPortTemplate(ModularComponentTemplateModel):
verbose_name = _('rear port template')
verbose_name_plural = _('rear port templates')
def clean(self):
super().clean()
# Check that positions is greater than or equal to the number of associated FrontPortTemplates
if not self._state.adding:
mapping_count = self.mappings.count()
if self.positions < mapping_count:
raise ValidationError({
"positions": _(
"The number of positions cannot be less than the number of mapped front port templates "
"({count})"
).format(count=mapping_count)
})
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),

View File

@@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
from dcim.constants import *
from dcim.fields import WWNField
from dcim.models.base import PortMappingBase
from dcim.models.mixins import InterfaceValidationMixin
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, NetBoxModel
@@ -35,6 +36,7 @@ __all__ = (
'InventoryItemRole',
'ModuleBay',
'PathEndpoint',
'PortMapping',
'PowerOutlet',
'PowerPort',
'RearPort',
@@ -1069,6 +1071,43 @@ class Interface(
# Pass-through ports
#
class PortMapping(PortMappingBase):
"""
Maps a FrontPort & position to a RearPort & position.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='port_mappings',
)
front_port = models.ForeignKey(
to='dcim.FrontPort',
on_delete=models.CASCADE,
related_name='mappings',
)
rear_port = models.ForeignKey(
to='dcim.RearPort',
on_delete=models.CASCADE,
related_name='mappings',
)
def clean(self):
super().clean()
# Both ports must belong to the same device
if self.front_port.device_id != self.rear_port.device_id:
raise ValidationError({
"rear_port": _("Rear port ({rear_port}) must belong to the same device").format(
rear_port=self.rear_port
)
})
def save(self, *args, **kwargs):
# Associate the mapping with the parent Device
self.device = self.front_port.device
super().save(*args, **kwargs)
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
"""
A pass-through port on the front of a Device.
@@ -1082,22 +1121,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
verbose_name=_('color'),
blank=True
)
rear_port = models.ForeignKey(
to='dcim.RearPort',
on_delete=models.CASCADE,
related_name='frontports'
)
rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
positions = models.PositiveSmallIntegerField(
verbose_name=_('positions'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
MaxValueValidator(REARPORT_POSITIONS_MAX)
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX)
],
help_text=_('Mapped position on corresponding rear port')
)
clone_fields = ('device', 'type', 'color')
clone_fields = ('device', 'type', 'color', 'positions')
class Meta(ModularComponentModel.Meta):
constraints = (
@@ -1105,10 +1138,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
fields=('device', 'name'),
name='%(app_label)s_%(class)s_unique_device_name'
),
models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='%(app_label)s_%(class)s_unique_rear_port_position'
),
)
verbose_name = _('front port')
verbose_name_plural = _('front ports')
@@ -1116,27 +1145,14 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
def clean(self):
super().clean()
if hasattr(self, 'rear_port'):
# Validate rear port assignment
if self.rear_port.device != self.device:
# Check that positions is greater than or equal to the number of associated RearPorts
if not self._state.adding:
mapping_count = self.mappings.count()
if self.positions < mapping_count:
raise ValidationError({
"rear_port": _(
"Rear port ({rear_port}) must belong to the same device"
).format(rear_port=self.rear_port)
})
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": _(
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
"positions."
).format(
rear_port_position=self.rear_port_position,
name=self.rear_port.name,
positions=self.rear_port.positions
)
"positions": _(
"The number of positions cannot be less than the number of mapped rear ports ({count})"
).format(count=mapping_count)
})
@@ -1157,11 +1173,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
verbose_name=_('positions'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
MaxValueValidator(REARPORT_POSITIONS_MAX)
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX)
],
help_text=_('Number of front ports which may be mapped')
)
clone_fields = ('device', 'type', 'color', 'positions')
class Meta(ModularComponentModel.Meta):
@@ -1173,13 +1189,13 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
# Check that positions count is greater than or equal to the number of associated FrontPorts
if not self._state.adding:
frontport_count = self.frontports.count()
if self.positions < frontport_count:
mapping_count = self.mappings.count()
if self.positions < mapping_count:
raise ValidationError({
"positions": _(
"The number of positions cannot be less than the number of mapped front ports "
"({frontport_count})"
).format(frontport_count=frontport_count)
"({count})"
).format(count=mapping_count)
})

View File

@@ -1,8 +1,7 @@
import decimal
import yaml
from functools import cached_property
import yaml
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
@@ -19,14 +18,14 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
from dcim.fields import MACAddressField
from dcim.utils import update_interface_bridges
from dcim.utils import create_port_mappings, update_interface_bridges
from extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet
from netbox.choices import ColorChoices
from netbox.config import ConfigItem
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
from netbox.models.mixins import WeightMixin
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from netbox.models.mixins import WeightMixin
from utilities.fields import ColorField, CounterCacheField
from utilities.prefetch import get_prefetchable_fields
from utilities.tracking import TrackingModelMixin
@@ -34,7 +33,6 @@ from .device_components import *
from .mixins import RenderConfigMixin
from .modules import Module
__all__ = (
'Device',
'DeviceRole',
@@ -1003,6 +1001,8 @@ class Device(
self._instantiate_components(self.device_type.interfacetemplates.all())
self._instantiate_components(self.device_type.rearporttemplates.all())
self._instantiate_components(self.device_type.frontporttemplates.all())
# Replicate any front/rear port mappings from the DeviceType
create_port_mappings(self, self.device_type)
# Disable bulk_create to accommodate MPTT
self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
self._instantiate_components(self.device_type.devicebaytemplates.all())

View File

@@ -150,15 +150,16 @@ def nullify_connected_endpoints(instance, **kwargs):
cablepath.retrace()
# TODO: Adapt signal handler to act on changes to port mappings
@receiver(post_save, sender=FrontPort)
def extend_rearport_cable_paths(instance, created, raw, **kwargs):
"""
When a new FrontPort is created, add it to any CablePaths which end at its corresponding RearPort.
"""
if created and not raw:
rearport = instance.rear_port
for cablepath in CablePath.objects.filter(_nodes__contains=rearport):
cablepath.retrace()
for mapping in instance.mappings.prefetch_related('rear_port'):
for cablepath in CablePath.objects.filter(_nodes__contains=mapping.rear_port):
cablepath.retrace()
@receiver(post_save, sender=Interface)

View File

@@ -749,12 +749,9 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
color = columns.ColorColumn(
verbose_name=_('Color'),
)
rear_port_position = tables.Column(
verbose_name=_('Position')
)
rear_port = tables.Column(
verbose_name=_('Rear Port'),
linkify=True
mappings = columns.ManyToManyColumn(
verbose_name=_('Mappings'),
transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}'
)
tags = columns.TagColumn(
url_name='dcim:frontport_list'
@@ -763,12 +760,12 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta):
model = models.FrontPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer',
'inventory_items', 'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created',
'last_updated',
)
default_columns = (
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description',
)
@@ -786,11 +783,11 @@ class DeviceFrontPortTable(FrontPortTable):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.FrontPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer',
)
@@ -805,6 +802,10 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
color = columns.ColorColumn(
verbose_name=_('Color'),
)
mappings = columns.ManyToManyColumn(
verbose_name=_('Mappings'),
transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}'
)
tags = columns.TagColumn(
url_name='dcim:rearport_list'
)
@@ -812,10 +813,13 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta):
model = models.RearPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created',
'last_updated',
)
default_columns = (
'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description',
)
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
class DeviceRearPortTable(RearPortTable):
@@ -832,11 +836,11 @@ class DeviceRearPortTable(RearPortTable):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.RearPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer',
'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer',
)

View File

@@ -249,12 +249,13 @@ class InterfaceTemplateTable(ComponentTemplateTable):
class FrontPortTemplateTable(ComponentTemplateTable):
rear_port_position = tables.Column(
verbose_name=_('Position')
)
color = columns.ColorColumn(
verbose_name=_('Color'),
)
mappings = columns.ManyToManyColumn(
verbose_name=_('Mappings'),
transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}'
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@@ -262,7 +263,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
class Meta(ComponentTemplateTable.Meta):
model = models.FrontPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'actions')
empty_text = "None"
@@ -270,6 +271,10 @@ class RearPortTemplateTable(ComponentTemplateTable):
color = columns.ColorColumn(
verbose_name=_('Color'),
)
mappings = columns.ManyToManyColumn(
verbose_name=_('Mappings'),
transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}'
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@@ -277,7 +282,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
class Meta(ComponentTemplateTable.Meta):
model = models.RearPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'actions')
empty_text = "None"

View File

@@ -973,69 +973,70 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 7', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=moduletype, name='Rear Port Template 8', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_port_templates)
front_port_templates = (
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[0]
),
FrontPortTemplate(
device_type=devicetype,
name='Front Port Template 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[1]
),
FrontPortTemplate(
module_type=moduletype,
name='Front Port Template 5',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[4]
),
FrontPortTemplate(
module_type=moduletype,
name='Front Port Template 6',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port_templates[5]
),
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
)
FrontPortTemplate.objects.bulk_create(front_port_templates)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(
device_type=devicetype,
front_port=front_port_templates[0],
rear_port=rear_port_templates[0],
),
PortTemplateMapping(
device_type=devicetype,
front_port=front_port_templates[1],
rear_port=rear_port_templates[1],
),
PortTemplateMapping(
module_type=moduletype,
front_port=front_port_templates[2],
rear_port=rear_port_templates[2],
),
])
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Front Port Template 3',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[2].pk,
'rear_port_position': 1,
'rear_ports': [
{
'position': 1,
'rear_port': rear_port_templates[3].pk,
'rear_port_position': 1,
},
],
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 4',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[3].pk,
'rear_port_position': 1,
'rear_ports': [
{
'position': 1,
'rear_port': rear_port_templates[4].pk,
'rear_port_position': 1,
},
],
},
{
'module_type': moduletype.pk,
'name': 'Front Port Template 7',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[6].pk,
'rear_port_position': 1,
},
{
'module_type': moduletype.pk,
'name': 'Front Port Template 8',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[7].pk,
'rear_port_position': 1,
'rear_ports': [
{
'position': 1,
'rear_port': rear_port_templates[5].pk,
'rear_port_position': 1,
},
],
},
]
@@ -1057,33 +1058,75 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
manufacturer=manufacturer, model='Module Type 1'
)
front_port_templates = (
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=moduletype, name='Front Port Template 4', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=moduletype, name='Front Port Template 5', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=moduletype, name='Front Port Template 6', type=PortTypeChoices.TYPE_8P8C),
)
FrontPortTemplate.objects.bulk_create(front_port_templates)
rear_port_templates = (
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_port_templates)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(
device_type=devicetype,
front_port=front_port_templates[0],
rear_port=rear_port_templates[0],
),
PortTemplateMapping(
device_type=devicetype,
front_port=front_port_templates[1],
rear_port=rear_port_templates[1],
),
PortTemplateMapping(
module_type=moduletype,
front_port=front_port_templates[2],
rear_port=rear_port_templates[2],
),
])
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Rear Port Template 4',
'type': PortTypeChoices.TYPE_8P8C,
'front_ports': [
{
'position': 1,
'front_port': front_port_templates[3].pk,
'front_port_position': 1,
},
],
},
{
'device_type': devicetype.pk,
'name': 'Rear Port Template 5',
'type': PortTypeChoices.TYPE_8P8C,
'front_ports': [
{
'position': 1,
'front_port': front_port_templates[4].pk,
'front_port_position': 1,
},
],
},
{
'module_type': moduletype.pk,
'name': 'Rear Port Template 6',
'type': PortTypeChoices.TYPE_8P8C,
},
{
'module_type': moduletype.pk,
'name': 'Rear Port Template 7',
'type': PortTypeChoices.TYPE_8P8C,
'front_ports': [
{
'position': 1,
'front_port': front_port_templates[5].pk,
'front_port_position': 1,
},
],
},
]
@@ -2015,51 +2058,64 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
RearPort(device=device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C),
)
RearPort.objects.bulk_create(rear_ports)
front_ports = (
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2]),
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
)
FrontPort.objects.bulk_create(front_ports)
PortMapping.objects.bulk_create([
PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
])
cls.create_data = [
{
'device': device.pk,
'name': 'Front Port 4',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_ports[3].pk,
'rear_port_position': 1,
'rear_ports': [
{
'position': 1,
'rear_port': rear_ports[3].pk,
'rear_port_position': 1,
},
],
},
{
'device': device.pk,
'name': 'Front Port 5',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_ports[4].pk,
'rear_port_position': 1,
'rear_ports': [
{
'position': 1,
'rear_port': rear_ports[4].pk,
'rear_port_position': 1,
},
],
},
{
'device': device.pk,
'name': 'Front Port 6',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_ports[5].pk,
'rear_port_position': 1,
'rear_ports': [
{
'position': 1,
'rear_port': rear_ports[5].pk,
'rear_port_position': 1,
},
],
},
]
@tag('regression') # Issue #18991
def test_front_port_paths(self):
device = Device.objects.first()
rear_port = RearPort.objects.create(
device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C
)
interface1 = Interface.objects.create(device=device, name='Interface 1')
front_port = FrontPort.objects.create(
device=device,
name='Rear Port 10',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_port,
)
rear_port = RearPort.objects.create(device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C)
front_port = FrontPort.objects.create(device=device, name='Front Port 10', type=PortTypeChoices.TYPE_8P8C)
PortMapping.objects.create(device=device, front_port=front_port, rear_port=rear_port)
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
@@ -2086,6 +2142,15 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
front_ports = (
FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 4', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 5', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=device, name='Front Port 6', type=PortTypeChoices.TYPE_8P8C),
)
FrontPort.objects.bulk_create(front_ports)
rear_ports = (
RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
RearPort(device=device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
@@ -2098,16 +2163,37 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
'device': device.pk,
'name': 'Rear Port 4',
'type': PortTypeChoices.TYPE_8P8C,
'front_ports': [
{
'position': 1,
'front_port': front_ports[3].pk,
'front_port_position': 1,
},
],
},
{
'device': device.pk,
'name': 'Rear Port 5',
'type': PortTypeChoices.TYPE_8P8C,
'front_ports': [
{
'position': 1,
'front_port': front_ports[4].pk,
'front_port_position': 1,
},
],
},
{
'device': device.pk,
'name': 'Rear Port 6',
'type': PortTypeChoices.TYPE_8P8C,
'front_ports': [
{
'position': 1,
'front_port': front_ports[5].pk,
'front_port_position': 1,
},
],
},
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
from unittest import skipIf
from circuits.models import CircuitTermination
from dcim.choices import CableProfileChoices
from dcim.models import *
@@ -363,13 +361,17 @@ class CablePathTests(CablePathTestCase):
Interface.objects.create(device=self.device, name='Interface 3'),
Interface.objects.create(device=self.device, name='Interface 4'),
]
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
frontport1 = FrontPort.objects.create(
device=self.device,
name='Front Port 1',
rear_port=rearport1,
rear_port_position=1
)
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=frontport1,
front_port_position=1,
rear_port=rearport1,
rear_port_position=1,
),
])
# Create cables
cable1 = Cable(
@@ -439,18 +441,40 @@ class CablePathTests(CablePathTestCase):
]
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
)
frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1')
frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2')
frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1')
frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2')
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=frontport1_1,
front_port_position=1,
rear_port=rearport1,
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=frontport1_2,
front_port_position=1,
rear_port=rearport1,
rear_port_position=2,
),
PortMapping(
device=self.device,
front_port=frontport2_1,
front_port_position=1,
rear_port=rearport2,
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=frontport2_2,
front_port_position=1,
rear_port=rearport2,
rear_port_position=2,
),
])
# Create cables
cable1 = Cable(
@@ -654,25 +678,47 @@ class CablePathTests(CablePathTestCase):
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),
RearPort.objects.create(device=self.device, name='Rear Port 1'),
RearPort.objects.create(device=self.device, name='Rear Port 2'),
RearPort.objects.create(device=self.device, name='Rear Port 3'),
RearPort.objects.create(device=self.device, name='Rear Port 4'),
]
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
),
FrontPort.objects.create(device=self.device, name='Front Port 1'),
FrontPort.objects.create(device=self.device, name='Front Port 2'),
FrontPort.objects.create(device=self.device, name='Front Port 3'),
FrontPort.objects.create(device=self.device, name='Front Port 4'),
]
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=front_ports[0],
front_port_position=1,
rear_port=rear_ports[0],
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=front_ports[1],
front_port_position=1,
rear_port=rear_ports[1],
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=front_ports[2],
front_port_position=1,
rear_port=rear_ports[2],
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=front_ports[3],
front_port_position=1,
rear_port=rear_ports[3],
rear_port_position=1,
),
])
# Create cables
cable1 = Cable(
@@ -723,8 +769,6 @@ class CablePathTests(CablePathTestCase):
# 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]
@@ -736,14 +780,26 @@ class CablePathTests(CablePathTestCase):
Interface.objects.create(device=self.device, name='Interface 3'),
Interface.objects.create(device=self.device, name='Interface 4'),
]
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', 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
)
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=frontport1,
front_port_position=1,
rear_port=rearport1,
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=frontport2,
front_port_position=1,
rear_port=rearport2,
rear_port_position=1,
),
])
# Create cables
cable1 = Cable(
@@ -761,9 +817,6 @@ class CablePathTests(CablePathTestCase):
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]),

View File

@@ -1355,22 +1355,15 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create(
(
FrontPortTemplate(
device_type=device_types[0],
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[0],
),
FrontPortTemplate(
device_type=device_types[1],
name='Front Port 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[1],
),
)
front_ports = (
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
])
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
@@ -1626,22 +1619,15 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
RearPortTemplate(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create(
(
FrontPortTemplate(
module_type=module_types[0],
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[0],
),
FrontPortTemplate(
module_type=module_types[1],
name='Front Port 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[1],
),
)
front_ports = (
FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(module_type=module_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(module_type=module_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
])
def test_q(self):
params = {'q': 'foobar1'}
@@ -2057,11 +2043,10 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create((
front_ports = (
FrontPortTemplate(
device_type=device_types[0],
name='Front Port 1',
rear_port=rear_ports[0],
type=PortTypeChoices.TYPE_8P8C,
color=ColorChoices.COLOR_RED,
description='foobar1'
@@ -2069,7 +2054,6 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
FrontPortTemplate(
device_type=device_types[1],
name='Front Port 2',
rear_port=rear_ports[1],
type=PortTypeChoices.TYPE_110_PUNCH,
color=ColorChoices.COLOR_GREEN,
description='foobar2'
@@ -2077,12 +2061,17 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
FrontPortTemplate(
device_type=device_types[2],
name='Front Port 3',
rear_port=rear_ports[2],
type=PortTypeChoices.TYPE_BNC,
color=ColorChoices.COLOR_BLUE,
description='foobar3'
),
))
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
PortTemplateMapping(device_type=device_types[2], front_port=front_ports[2], rear_port=rear_ports[2]),
])
def test_name(self):
params = {'name': ['Front Port 1', 'Front Port 2']}
@@ -2752,10 +2741,15 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPort.objects.bulk_create(rear_ports)
FrontPort.objects.bulk_create((
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
front_ports = (
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
)
FrontPort.objects.bulk_create(front_ports)
PortMapping.objects.bulk_create([
PortMapping(device=devices[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortMapping(device=devices[1], front_port=front_ports[1], rear_port=rear_ports[1]),
])
ModuleBay.objects.create(device=devices[0], name='Module Bay 1')
ModuleBay.objects.create(device=devices[1], name='Module Bay 2')
DeviceBay.objects.bulk_create((
@@ -5090,8 +5084,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
label='A',
type=PortTypeChoices.TYPE_8P8C,
color=ColorChoices.COLOR_RED,
rear_port=rear_ports[0],
rear_port_position=1,
description='First',
_site=devices[0].site,
_location=devices[0].location,
@@ -5104,8 +5096,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
label='B',
type=PortTypeChoices.TYPE_110_PUNCH,
color=ColorChoices.COLOR_GREEN,
rear_port=rear_ports[1],
rear_port_position=2,
description='Second',
_site=devices[1].site,
_location=devices[1].location,
@@ -5118,8 +5108,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
label='C',
type=PortTypeChoices.TYPE_BNC,
color=ColorChoices.COLOR_BLUE,
rear_port=rear_ports[2],
rear_port_position=3,
description='Third',
_site=devices[2].site,
_location=devices[2].location,
@@ -5130,8 +5118,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
name='Front Port 4',
label='D',
type=PortTypeChoices.TYPE_FC,
rear_port=rear_ports[3],
rear_port_position=1,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
@@ -5141,8 +5127,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
name='Front Port 5',
label='E',
type=PortTypeChoices.TYPE_FC,
rear_port=rear_ports[4],
rear_port_position=1,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
@@ -5152,14 +5136,20 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
name='Front Port 6',
label='F',
type=PortTypeChoices.TYPE_FC,
rear_port=rear_ports[5],
rear_port_position=1,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
),
)
FrontPort.objects.bulk_create(front_ports)
PortMapping.objects.bulk_create([
PortMapping(device=devices[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortMapping(device=devices[1], front_port=front_ports[1], rear_port=rear_ports[1], rear_port_position=2),
PortMapping(device=devices[2], front_port=front_ports[2], rear_port=rear_ports[2], rear_port_position=3),
PortMapping(device=devices[3], front_port=front_ports[3], rear_port=rear_ports[3]),
PortMapping(device=devices[3], front_port=front_ports[4], rear_port=rear_ports[4]),
PortMapping(device=devices[3], front_port=front_ports[5], rear_port=rear_ports[5]),
])
# Cables
Cable(a_terminations=[front_ports[0]], b_terminations=[front_ports[3]]).save()
@@ -6420,13 +6410,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
power_port = PowerPort.objects.create(device=devices[0], name='Power Port 1')
power_outlet = PowerOutlet.objects.create(device=devices[0], name='Power Outlet 1')
rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1', positions=1)
front_port = FrontPort.objects.create(
device=devices[0],
name='Front Port 1',
rear_port=rear_port,
rear_port_position=1
)
rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1')
front_port = FrontPort.objects.create(device=devices[0], name='Front Port 1')
PortMapping.objects.create(device=devices[0], front_port=front_port, rear_port=rear_port)
power_panel = PowerPanel.objects.create(name='Power Panel 1', site=sites[0])
power_feed = PowerFeed.objects.create(name='Power Feed 1', power_panel=power_panel)

View File

@@ -193,7 +193,8 @@ class FrontPortTestCase(TestCase):
'name': 'FrontPort[1-4]',
'label': 'Port[1-4]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
'positions': 1,
'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
}
form = FrontPortCreateForm(front_port_data)
@@ -208,7 +209,8 @@ class FrontPortTestCase(TestCase):
'name': 'FrontPort[1-4]',
'label': 'Port[1-2]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
'positions': 1,
'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
}
form = FrontPortCreateForm(bad_front_port_data)

View File

@@ -444,13 +444,19 @@ class DeviceTestCase(TestCase):
)
rearport.save()
FrontPortTemplate(
frontport = FrontPortTemplate(
device_type=device_type,
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
)
frontport.save()
PortTemplateMapping.objects.create(
device_type=device_type,
front_port=frontport,
rear_port=rearport,
rear_port_position=2
).save()
rear_port_position=2,
)
ModuleBayTemplate(
device_type=device_type,
@@ -528,11 +534,12 @@ class DeviceTestCase(TestCase):
device=device,
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rearport,
rear_port_position=2
positions=1
)
self.assertEqual(frontport.cf['cf1'], 'foo')
self.assertTrue(PortMapping.objects.filter(front_port=frontport, rear_port=rearport).exists())
modulebay = ModuleBay.objects.get(
device=device,
name='Module Bay 1'
@@ -835,12 +842,18 @@ class CableTestCase(TestCase):
)
RearPort.objects.bulk_create(rear_ports)
front_ports = (
FrontPort(device=patch_panel, name='FP1', type='8p8c', rear_port=rear_ports[0], rear_port_position=1),
FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1),
FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1),
FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1),
FrontPort(device=patch_panel, name='FP1', type='8p8c'),
FrontPort(device=patch_panel, name='FP2', type='8p8c'),
FrontPort(device=patch_panel, name='FP3', type='8p8c'),
FrontPort(device=patch_panel, name='FP4', type='8p8c'),
)
FrontPort.objects.bulk_create(front_ports)
PortMapping.objects.bulk_create([
PortMapping(device=patch_panel, front_port=front_ports[0], rear_port=rear_ports[0]),
PortMapping(device=patch_panel, front_port=front_ports[1], rear_port=rear_ports[1]),
PortMapping(device=patch_panel, front_port=front_ports[2], rear_port=rear_ports[2]),
PortMapping(device=patch_panel, front_port=front_ports[3], rear_port=rear_ports[3]),
])
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)

View File

@@ -741,17 +741,16 @@ class DeviceTypeTestCase(
)
RearPortTemplate.objects.bulk_create(rear_ports)
front_ports = (
FrontPortTemplate(
device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
),
FrontPortTemplate(
device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
),
FrontPortTemplate(
device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
),
FrontPortTemplate(device_type=devicetype, name='Front Port 1'),
FrontPortTemplate(device_type=devicetype, name='Front Port 2'),
FrontPortTemplate(device_type=devicetype, name='Front Port 3'),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]),
PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]),
])
url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@@ -866,12 +865,16 @@ rear-ports:
front-ports:
- name: Front Port 1
type: 8p8c
rear_port: Rear Port 1
- name: Front Port 2
type: 8p8c
rear_port: Rear Port 2
- name: Front Port 3
type: 8p8c
port-mappings:
- front_port: Front Port 1
rear_port: Rear Port 1
- front_port: Front Port 2
rear_port: Rear Port 2
- front_port: Front Port 3
rear_port: Rear Port 3
module-bays:
- name: Module Bay 1
@@ -971,8 +974,12 @@ inventory-items:
self.assertEqual(device_type.frontporttemplates.count(), 3)
fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(device_type.port_mappings.count(), 3)
mapping1 = PortTemplateMapping.objects.first()
self.assertEqual(mapping1.device_type, device_type)
self.assertEqual(mapping1.front_port, fp1)
self.assertEqual(mapping1.rear_port, rp1)
self.assertEqual(device_type.modulebaytemplates.count(), 3)
mb1 = ModuleBayTemplate.objects.first()
@@ -1316,17 +1323,16 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
RearPortTemplate.objects.bulk_create(rear_ports)
front_ports = (
FrontPortTemplate(
module_type=moduletype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
),
FrontPortTemplate(
module_type=moduletype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
),
FrontPortTemplate(
module_type=moduletype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
),
FrontPortTemplate(module_type=moduletype, name='Front Port 1'),
FrontPortTemplate(module_type=moduletype, name='Front Port 2'),
FrontPortTemplate(module_type=moduletype, name='Front Port 3'),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(module_type=moduletype, front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(module_type=moduletype, front_port=front_ports[1], rear_port=rear_ports[1]),
PortTemplateMapping(module_type=moduletype, front_port=front_ports[2], rear_port=rear_ports[2]),
])
url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@@ -1394,12 +1400,16 @@ rear-ports:
front-ports:
- name: Front Port 1
type: 8p8c
rear_port: Rear Port 1
- name: Front Port 2
type: 8p8c
rear_port: Rear Port 2
- name: Front Port 3
type: 8p8c
port-mappings:
- front_port: Front Port 1
rear_port: Rear Port 1
- front_port: Front Port 2
rear_port: Rear Port 2
- front_port: Front Port 3
rear_port: Rear Port 3
module-bays:
- name: Module Bay 1
@@ -1477,8 +1487,12 @@ module-bays:
self.assertEqual(module_type.frontporttemplates.count(), 3)
fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(module_type.port_mappings.count(), 3)
mapping1 = PortTemplateMapping.objects.first()
self.assertEqual(mapping1.module_type, module_type)
self.assertEqual(mapping1.front_port, fp1)
self.assertEqual(mapping1.rear_port, rp1)
self.assertEqual(module_type.modulebaytemplates.count(), 3)
mb1 = ModuleBayTemplate.objects.first()
@@ -1770,7 +1784,7 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
rearports = (
rear_ports = (
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
@@ -1778,35 +1792,33 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'),
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'),
)
RearPortTemplate.objects.bulk_create(rearports)
FrontPortTemplate.objects.bulk_create(
(
FrontPortTemplate(
device_type=devicetype, name='Front Port Template 1', rear_port=rearports[0], rear_port_position=1
),
FrontPortTemplate(
device_type=devicetype, name='Front Port Template 2', rear_port=rearports[1], rear_port_position=1
),
FrontPortTemplate(
device_type=devicetype, name='Front Port Template 3', rear_port=rearports[2], rear_port_position=1
),
)
RearPortTemplate.objects.bulk_create(rear_ports)
front_ports = (
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1'),
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2'),
FrontPortTemplate(device_type=devicetype, name='Front Port Template 3'),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]),
PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]),
])
cls.form_data = {
'device_type': devicetype.pk,
'name': 'Front Port X',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rearports[3].pk,
'rear_port_position': 1,
'positions': 1,
'rear_ports': [f'{rear_ports[3].pk}:1'],
}
cls.bulk_create_data = {
'device_type': devicetype.pk,
'name': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
'positions': 1,
'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]],
}
cls.bulk_edit_data = {
@@ -2276,11 +2288,16 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
RearPort.objects.bulk_create(rear_ports)
front_ports = (
FrontPort(device=device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
FrontPort(device=device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
FrontPort(device=device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
FrontPort(device=device, name='Front Port Template 1'),
FrontPort(device=device, name='Front Port Template 2'),
FrontPort(device=device, name='Front Port Template 3'),
)
FrontPort.objects.bulk_create(front_ports)
PortMapping.objects.bulk_create([
PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
])
url = reverse('dcim:device_frontports', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
@@ -3065,7 +3082,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
def setUpTestData(cls):
device = create_test_device('Device 1')
rearports = (
rear_ports = (
RearPort(device=device, name='Rear Port 1'),
RearPort(device=device, name='Rear Port 2'),
RearPort(device=device, name='Rear Port 3'),
@@ -3073,14 +3090,19 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
RearPort(device=device, name='Rear Port 5'),
RearPort(device=device, name='Rear Port 6'),
)
RearPort.objects.bulk_create(rearports)
RearPort.objects.bulk_create(rear_ports)
front_ports = (
FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]),
FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]),
FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
FrontPort(device=device, name='Front Port 1'),
FrontPort(device=device, name='Front Port 2'),
FrontPort(device=device, name='Front Port 3'),
)
FrontPort.objects.bulk_create(front_ports)
PortMapping.objects.bulk_create([
PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -3088,8 +3110,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
'device': device.pk,
'name': 'Front Port X',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rearports[3].pk,
'rear_port_position': 1,
'positions': 1,
'rear_ports': [f'{rear_ports[3].pk}:1'],
'description': 'New description',
'tags': [t.pk for t in tags],
}
@@ -3098,7 +3120,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
'device': device.pk,
'name': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
'positions': 1,
'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]],
'description': 'New description',
'tags': [t.pk for t in tags],
}
@@ -3109,10 +3132,10 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
"device,name,type,rear_port,rear_port_position",
"Device 1,Front Port 4,8p8c,Rear Port 4,1",
"Device 1,Front Port 5,8p8c,Rear Port 5,1",
"Device 1,Front Port 6,8p8c,Rear Port 6,1",
"device,name,type,positions",
"Device 1,Front Port 4,8p8c,1",
"Device 1,Front Port 5,8p8c,1",
"Device 1,Front Port 6,8p8c,1",
)
cls.csv_update_data = (

View File

@@ -83,3 +83,36 @@ def update_interface_bridges(device, interface_templates, module=None):
)
interface.full_clean()
interface.save()
def create_port_mappings(device, device_type, module=None):
"""
Replicate all front/rear port mappings from a DeviceType to the given device.
"""
from dcim.models import FrontPort, PortMapping, RearPort
templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port')
# Cache front & rear ports for efficient lookups by name
front_ports = {
fp.name: fp for fp in FrontPort.objects.filter(device=device)
}
rear_ports = {
rp.name: rp for rp in RearPort.objects.filter(device=device)
}
# Replicate PortMappings
mappings = []
for template in templates:
front_port = front_ports.get(template.front_port.resolve_name(module=module))
rear_port = rear_ports.get(template.rear_port.resolve_name(module=module))
mappings.append(
PortMapping(
device_id=front_port.device_id,
front_port=front_port,
front_port_position=template.front_port_position,
rear_port=rear_port,
rear_port_position=template.rear_port_position,
)
)
PortMapping.objects.bulk_create(mappings)

View File

@@ -42,6 +42,7 @@ from wireless.models import WirelessLAN
from . import filtersets, forms, tables
from .choices import DeviceFaceChoices, InterfaceModeChoices
from .models import *
from .models.device_components import PortMapping
from .object_actions import BulkAddComponents, BulkDisconnect
CABLE_TERMINATION_TYPES = {
@@ -1515,6 +1516,7 @@ class DeviceTypeImportView(generic.BulkImportView):
'interfaces': forms.InterfaceTemplateImportForm,
'rear-ports': forms.RearPortTemplateImportForm,
'front-ports': forms.FrontPortTemplateImportForm,
'port-mappings': forms.PortTemplateMappingImportForm,
'module-bays': forms.ModuleBayTemplateImportForm,
'device-bays': forms.DeviceBayTemplateImportForm,
'inventory-items': forms.InventoryItemTemplateImportForm,
@@ -1819,6 +1821,7 @@ class ModuleTypeImportView(generic.BulkImportView):
'interfaces': forms.InterfaceTemplateImportForm,
'rear-ports': forms.RearPortTemplateImportForm,
'front-ports': forms.FrontPortTemplateImportForm,
'port-mappings': forms.PortTemplateMappingImportForm,
'module-bays': forms.ModuleBayTemplateImportForm,
}
@@ -3242,6 +3245,11 @@ class FrontPortListView(generic.ObjectListView):
class FrontPortView(generic.ObjectView):
queryset = FrontPort.objects.all()
def get_extra_context(self, request, instance):
return {
'rear_port_mappings': PortMapping.objects.filter(front_port=instance).prefetch_related('rear_port'),
}
@register_model_view(FrontPort, 'add', detail=False)
class FrontPortCreateView(generic.ComponentCreateView):
@@ -3313,6 +3321,11 @@ class RearPortListView(generic.ObjectListView):
class RearPortView(generic.ObjectView):
queryset = RearPort.objects.all()
def get_extra_context(self, request, instance):
return {
'front_port_mappings': PortMapping.objects.filter(rear_port=instance).prefetch_related('front_port'),
}
@register_model_view(RearPort, 'add', detail=False)
class RearPortCreateView(generic.ComponentCreateView):

View File

@@ -1,6 +1,5 @@
import logging
from collections import defaultdict
from copy import deepcopy
from django.contrib import messages
from django.db import router, transaction
@@ -563,7 +562,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
if form.is_valid():
new_components = []
data = deepcopy(request.POST)
data = request.POST.copy()
pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
for i in range(pattern_count):
@@ -572,7 +571,8 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
data[field_name] = form.cleaned_data[field_name][i]
if hasattr(form, 'get_iterative_data'):
data.update(form.get_iterative_data(i))
for k, v in form.get_iterative_data(i).items():
data.setlist(k, v)
component_form = self.model_form(data)

View File

@@ -47,12 +47,8 @@
</td>
</tr>
<tr>
<th scope="row">{% trans "Rear Port" %}</th>
<td>{{ object.rear_port|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Rear Port Position" %}</th>
<td>{{ object.rear_port_position }}</td>
<th scope="row">{% trans "Positions" %}</th>
<td>{{ object.positions }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
@@ -65,6 +61,18 @@
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rear Ports" %}</h2>
<table class="table table-hover">
{% for mapping in rear_port_mappings %}
<tr>
<td>{{ mapping.front_port_position }}</td>
<td>{{ mapping.rear_port|linkify }}</td>
<td>{{ mapping.rear_port_position }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -61,6 +61,18 @@
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rear Ports" %}</h2>
<table class="table table-hover">
{% for mapping in front_port_mappings %}
<tr>
<td>{{ mapping.rear_port_position }}</td>
<td>{{ mapping.front_port|linkify }}</td>
<td>{{ mapping.front_port_position }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -13,7 +13,7 @@ def get_related_models(model, ordered=True):
related_models = [
(field.related_model, field.remote_field.name)
for field in model._meta.related_objects
if type(field) is ManyToOneRel
if type(field) is ManyToOneRel and not getattr(field.related_model, '_netbox_private', False)
]
if ordered: