diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py
index 1dca773b2..3b9142f1d 100644
--- a/netbox/dcim/api/serializers_/base.py
+++ b/netbox/dcim/api/serializers_/base.py
@@ -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
diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py
index b26cf9bbb..99940b942 100644
--- a/netbox/dcim/api/serializers_/device_components.py
+++ b/netbox/dcim/api/serializers_/device_components.py
@@ -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')
diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py
index b44565d65..9a9b5d470 100644
--- a/netbox/dcim/api/serializers_/devicetype_components.py
+++ b/netbox/dcim/api/serializers_/devicetype_components.py
@@ -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')
diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
index 5e4311c13..16926081f 100644
--- a/netbox/dcim/constants.py
+++ b/netbox/dcim/constants.py
@@ -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
#
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index a3f8ff383..0cd75f954 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -904,12 +904,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')
@register_filterset
@@ -918,6 +921,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
@@ -2137,13 +2146,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',
)
@@ -2154,6 +2166,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
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index ba0b44b0d..68258aa8b 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -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(
diff --git a/netbox/dcim/forms/mixins.py b/netbox/dcim/forms/mixins.py
index 96eb8a56b..b2fc46bc3 100644
--- a/netbox/dcim/forms/mixins.py
+++ b/netbox/dcim/forms/mixins.py
@@ -1,10 +1,12 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.db import connection
+from django.db.models.signals import post_save
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 +15,7 @@ from utilities.templatetags.builtins.filters import bettertitle
from utilities.forms.widgets import HTMXSelect
__all__ = (
+ 'FrontPortFormMixin',
'ScopedBulkEditForm',
'ScopedForm',
'ScopedImportForm',
@@ -128,3 +131,75 @@ 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()
+
+ # Check that the total number of FrontPorts and positions matches the selected number of RearPort:position
+ # mappings. Note that `name` will be a list under FrontPortCreateForm, in which cases we multiply the number of
+ # FrontPorts being creation by the number of positions.
+ positions = self.cleaned_data['positions']
+ frontport_count = len(self.cleaned_data['name']) if type(self.cleaned_data['name']) is list else 1
+ rearport_count = len(self.cleaned_data['rear_ports'])
+ if frontport_count * positions != rearport_count:
+ raise forms.ValidationError({
+ 'rear_ports': _(
+ "The total number of front port positions ({frontport_count}) must match the selected number of "
+ "rear port positions ({rearport_count})."
+ ).format(
+ frontport_count=frontport_count,
+ rearport_count=rearport_count
+ )
+ })
+
+ 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)
+ # Send post_save signals
+ for mapping in mappings:
+ post_save.send(
+ sender=PortMapping,
+ instance=mapping,
+ created=True,
+ raw=False,
+ using=connection,
+ update_fields=None
+ )
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index 66e54b887..709ecdfe0 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -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 = (
diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py
index d9afabddf..8e0818326 100644
--- a/netbox/dcim/forms/object_create.py
+++ b/netbox/dcim/forms/object_create.py
@@ -109,85 +109,30 @@ 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
-
- def clean(self):
- super().clean()
-
- # Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
- # positions
- frontport_count = len(self.cleaned_data['name'])
- rearport_count = len(self.cleaned_data['rear_port'])
- if frontport_count != rearport_count:
- raise forms.ValidationError({
- 'rear_port': _(
- "The number of front port templates to be created ({frontport_count}) must match the selected "
- "number of rear port positions ({rearport_count})."
- ).format(
- frontport_count=frontport_count,
- rearport_count=rearport_count
- )
- })
+ class Meta:
+ model = FrontPortTemplate
+ fields = (
+ 'device_type', 'module_type', 'type', 'color', 'positions', 'description',
+ )
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,74 +214,26 @@ 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()
-
- # Check that the number of FrontPorts to be created matches the selected number of RearPort positions
- frontport_count = len(self.cleaned_data['name'])
- rearport_count = len(self.cleaned_data['rear_port'])
- if frontport_count != rearport_count:
- raise forms.ValidationError({
- 'rear_port': _(
- "The number of front ports to be created ({frontport_count}) must match the selected number of "
- "rear port positions ({rearport_count})."
- ).format(
- frontport_count=frontport_count,
- rearport_count=rearport_count
- )
- })
-
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]
}
diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py
index 3f2cc3ef6..3b6a6e648 100644
--- a/netbox/dcim/forms/object_import.py
+++ b/netbox/dcim/forms/object_import.py
@@ -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:
diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py
index 1c99beedb..11aff8201 100644
--- a/netbox/dcim/graphql/filters.py
+++ b/netbox/dcim/graphql/filters.py
@@ -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)
diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py
index 13408dc90..1132a0ca9 100644
--- a/netbox/dcim/graphql/types.py
+++ b/netbox/dcim/graphql/types.py
@@ -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(
diff --git a/netbox/dcim/migrations/0222_port_mappings.py b/netbox/dcim/migrations/0222_port_mappings.py
new file mode 100644
index 000000000..a163ae18d
--- /dev/null
+++ b/netbox/dcim/migrations/0222_port_mappings.py
@@ -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
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0223_frontport_positions.py b/netbox/dcim/migrations/0223_frontport_positions.py
new file mode 100644
index 000000000..fc3394738
--- /dev/null
+++ b/netbox/dcim/migrations/0223_frontport_positions.py
@@ -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)
+ ]
+ ),
+ ),
+ ]
diff --git a/netbox/dcim/models/base.py b/netbox/dcim/models/base.py
new file mode 100644
index 000000000..f8021d4db
--- /dev/null
+++ b/netbox/dcim/models/base.py
@@ -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
+ )
+ })
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index b55b114c0..e75b4c110 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -1,4 +1,5 @@
import itertools
+import logging
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -22,7 +23,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',
@@ -30,6 +31,8 @@ __all__ = (
'CableTermination',
)
+logger = logging.getLogger(f'netbox.{__name__}')
+
trace_paths = Signal()
@@ -666,7 +669,13 @@ class CablePath(models.Model):
is_active = True
is_split = False
+ logger.debug(f'Tracing cable path from {terminations}...')
+
+ segment = 0
while terminations:
+ segment += 1
+ logger.debug(f'[Path segment #{segment}] Position stack: {position_stack}')
+ logger.debug(f'[Path segment #{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 +706,10 @@ 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
+ ))
+ logger.debug(f'[Path segment #{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,10 +772,13 @@ class CablePath(models.Model):
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
]
+ logger.debug(f'[Path segment #{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
is_split = True
+ logger.debug('Remote termination types differ; aborting trace.')
break
# Step 7: Record the far-end termination object(s)
@@ -777,58 +792,53 @@ 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
+ logger.debug(
+ 'Encountered front port mapped to multiple rear ports but position stack is empty; aborting '
+ 'trace.'
+ )
+ 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
+ logger.debug(
+ 'Encountered rear port mapped to multiple front ports but position stack is empty; aborting '
+ 'trace.'
+ )
+ 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)
@@ -876,6 +886,7 @@ class CablePath(models.Model):
# Unsupported topology, mark as split and exit
is_complete = False
is_split = True
+ logger.warning('Encountered an unsupported topology; aborting trace.')
break
return cls(
@@ -954,16 +965,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):
"""
diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py
index 74e624d6c..16e281523 100644
--- a/netbox/dcim/models/device_component_templates.py
+++ b/netbox/dcim/models/device_component_templates.py
@@ -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')),
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 8c9acc48f..e2077e9fe 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -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',
@@ -208,10 +210,6 @@ class CabledObjectModel(models.Model):
raise ValidationError({
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
})
- if not self.cable_position:
- raise ValidationError({
- "cable_position": _("Must specify cable termination position when attaching a cable.")
- })
if self.cable_end and not self.cable:
raise ValidationError({
"cable_end": _("Cable end must not be set without a cable.")
@@ -1069,6 +1067,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 +1117,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 +1134,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 +1141,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 +1169,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 +1185,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)
})
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 5e67f1ea8..423265751 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -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())
diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py
index eb1825c1a..5ec1f68d7 100644
--- a/netbox/dcim/signals.py
+++ b/netbox/dcim/signals.py
@@ -1,5 +1,6 @@
import logging
+from django.db.models import Q
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
@@ -7,7 +8,7 @@ from dcim.choices import CableEndChoices, LinkStatusChoices
from virtualization.models import VMInterface
from .models import (
Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
- InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
+ InventoryItem, ModuleBay, PathEndpoint, PortMapping, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
VirtualChassis,
)
from .models.cables import trace_paths
@@ -135,6 +136,17 @@ def retrace_cable_paths(instance, **kwargs):
cablepath.retrace()
+@receiver((post_delete, post_save), sender=PortMapping)
+def update_passthrough_port_paths(instance, **kwargs):
+ """
+ When a PortMapping is created or deleted, retrace any CablePaths which traverse its front and/or rear ports.
+ """
+ for cablepath in CablePath.objects.filter(
+ Q(_nodes__contains=instance.front_port) | Q(_nodes__contains=instance.rear_port)
+ ):
+ cablepath.retrace()
+
+
@receiver(post_delete, sender=CableTermination)
def nullify_connected_endpoints(instance, **kwargs):
"""
@@ -150,17 +162,6 @@ def nullify_connected_endpoints(instance, **kwargs):
cablepath.retrace()
-@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()
-
-
@receiver(post_save, sender=Interface)
@receiver(post_save, sender=VMInterface)
def update_mac_address_interface(instance, created, raw, **kwargs):
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 5f9467297..d81718d78 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -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',
)
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 979689b75..b7ad758df 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -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"
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index bdade5395..d4783bc3c 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -973,72 +973,99 @@ 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,
+ },
+ ],
},
]
+ cls.update_data = {
+ 'type': PortTypeChoices.TYPE_LC,
+ 'rear_ports': [
+ {
+ 'position': 1,
+ 'rear_port': rear_port_templates[3].pk,
+ 'rear_port_position': 1,
+ },
+ ],
+ }
+
+ def test_update_object(self):
+ super().test_update_object()
+
+ # Check that the port mapping was updated after modifying the front port template
+ front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 1')
+ rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 4')
+ self.assertTrue(
+ PortTemplateMapping.objects.filter(
+ front_port=front_port_template,
+ front_port_position=1,
+ rear_port=rear_port_template,
+ rear_port_position=1,
+ ).exists()
+ )
+
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = RearPortTemplate
@@ -1057,36 +1084,104 @@ 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,
+ },
+ ],
},
]
+ cls.update_data = {
+ 'type': PortTypeChoices.TYPE_LC,
+ 'front_ports': [
+ {
+ 'position': 1,
+ 'front_port': front_port_templates[3].pk,
+ 'front_port_position': 1,
+ },
+ ],
+ }
+
+ def test_update_object(self):
+ super().test_update_object()
+
+ # Check that the port mapping was updated after modifying the rear port template
+ front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 4')
+ rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 1')
+ self.assertTrue(
+ PortTemplateMapping.objects.filter(
+ front_port=front_port_template,
+ front_port_position=1,
+ rear_port=rear_port_template,
+ rear_port_position=1,
+ ).exists()
+ )
+
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = ModuleBayTemplate
@@ -2015,51 +2110,90 @@ 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,
+ },
+ ],
},
]
+ cls.update_data = {
+ 'type': PortTypeChoices.TYPE_LC,
+ 'rear_ports': [
+ {
+ 'position': 1,
+ 'rear_port': rear_ports[3].pk,
+ 'rear_port_position': 1,
+ },
+ ],
+ }
+
+ def test_update_object(self):
+ super().test_update_object()
+
+ # Check that the port mapping was updated after modifying the front port
+ front_port = FrontPort.objects.get(name='Front Port 1')
+ rear_port = RearPort.objects.get(name='Rear Port 4')
+ self.assertTrue(
+ PortMapping.objects.filter(
+ front_port=front_port,
+ front_port_position=1,
+ rear_port=rear_port,
+ rear_port_position=1,
+ ).exists()
+ )
+
@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 +2220,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,19 +2241,66 @@ 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,
+ },
+ ],
},
]
+ cls.update_data = {
+ 'type': PortTypeChoices.TYPE_LC,
+ 'front_ports': [
+ {
+ 'position': 1,
+ 'front_port': front_ports[3].pk,
+ 'front_port_position': 1,
+ },
+ ],
+ }
+
+ def test_update_object(self):
+ super().test_update_object()
+
+ # Check that the port mapping was updated after modifying the rear port
+ front_port = FrontPort.objects.get(name='Front Port 4')
+ rear_port = RearPort.objects.get(name='Rear Port 1')
+ self.assertTrue(
+ PortMapping.objects.filter(
+ front_port=front_port,
+ front_port_position=1,
+ rear_port=rear_port,
+ rear_port_position=1,
+ ).exists()
+ )
+
@tag('regression') # Issue #18991
def test_rear_port_paths(self):
device = Device.objects.first()
diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py
index ed31dd0f4..1bd613e3b 100644
--- a/netbox/dcim/tests/test_cablepaths.py
+++ b/netbox/dcim/tests/test_cablepaths.py
@@ -281,9 +281,14 @@ class LegacyCablePathTests(CablePathTestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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.create(
+ device=self.device,
+ front_port=frontport1,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=1
)
# Create cable 1
@@ -340,9 +345,14 @@ class LegacyCablePathTests(CablePathTestCase):
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
- rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
- 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.create(
+ device=self.device,
+ front_port=frontport1,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=1
)
# Create cable 1
@@ -403,18 +413,40 @@ class LegacyCablePathTests(CablePathTestCase):
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
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 1-2
cable1 = Cable(
@@ -521,18 +553,40 @@ class LegacyCablePathTests(CablePathTestCase):
interface8 = Interface.objects.create(device=self.device, name='Interface 8')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
- frontport1_1 = FrontPort.objects.create(
- device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
- )
- frontport1_2 = FrontPort.objects.create(
- device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
- )
- frontport2_1 = FrontPort.objects.create(
- device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
- )
- frontport2_2 = FrontPort.objects.create(
- device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
- )
+ 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 1-2
cable1 = Cable(
@@ -680,27 +734,59 @@ class LegacyCablePathTests(CablePathTestCase):
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
- rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
- rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
+ rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3')
rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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 = FrontPort.objects.create(
- device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
- )
- frontport3 = FrontPort.objects.create(
- device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
- )
- frontport4_1 = FrontPort.objects.create(
- device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1
- )
- frontport4_2 = FrontPort.objects.create(
- device=self.device, name='Front Port 4:2', rear_port=rearport4, 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 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3')
+ frontport4_1 = FrontPort.objects.create(device=self.device, name='Front Port 4:1')
+ frontport4_2 = FrontPort.objects.create(device=self.device, name='Front Port 4: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,
+ front_port_position=1,
+ rear_port=rearport2,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4_1,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4_2,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=2,
+ ),
+ ])
# Create cables 1-2, 6-7
cable1 = Cable(
@@ -801,30 +887,72 @@ class LegacyCablePathTests(CablePathTestCase):
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4)
rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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
- )
- frontport3_1 = FrontPort.objects.create(
- device=self.device, name='Front Port 3:1', rear_port=rearport3, rear_port_position=1
- )
- frontport3_2 = FrontPort.objects.create(
- device=self.device, name='Front Port 3:2', rear_port=rearport3, rear_port_position=2
- )
- frontport4_1 = FrontPort.objects.create(
- device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1
- )
- frontport4_2 = FrontPort.objects.create(
- device=self.device, name='Front Port 4:2', rear_port=rearport4, 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')
+ frontport3_1 = FrontPort.objects.create(device=self.device, name='Front Port 3:1')
+ frontport3_2 = FrontPort.objects.create(device=self.device, name='Front Port 3:2')
+ frontport4_1 = FrontPort.objects.create(device=self.device, name='Front Port 4:1')
+ frontport4_2 = FrontPort.objects.create(device=self.device, name='Front Port 4: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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3_1,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3_2,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=2,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4_1,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4_2,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=2,
+ ),
+ ])
# Create cables 1-3, 6-8
cable1 = Cable(
@@ -928,23 +1056,50 @@ class LegacyCablePathTests(CablePathTestCase):
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
- rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1)
- rearport3 = 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 = FrontPort.objects.create(
- device=self.device, name='Front Port 5', rear_port=rearport2, rear_port_position=1
- )
- frontport3_1 = FrontPort.objects.create(
- device=self.device, name='Front Port 2:1', rear_port=rearport3, rear_port_position=1
- )
- frontport3_2 = FrontPort.objects.create(
- device=self.device, name='Front Port 2:2', rear_port=rearport3, rear_port_position=2
- )
+ rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4)
+ 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 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3_1 = FrontPort.objects.create(device=self.device, name='Front Port 3:1')
+ frontport3_2 = FrontPort.objects.create(device=self.device, name='Front Port 3: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,
+ front_port_position=1,
+ rear_port=rearport2,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3_1,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3_2,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=2,
+ ),
+ ])
# Create cables 1-2, 5-6
cable1 = Cable(
@@ -1032,13 +1187,25 @@ class LegacyCablePathTests(CablePathTestCase):
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
- rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', 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
- )
+ rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=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')
+ 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,
+ ),
+ ])
# Create cables 1
cable1 = Cable(
@@ -1098,10 +1265,11 @@ class LegacyCablePathTests(CablePathTestCase):
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
- 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
+ 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')
+ PortMapping.objects.create(
+ front_port=frontport1, front_port_position=1, rear_port=rearport1, rear_port_position=1,
)
# Create cables
@@ -1413,18 +1581,40 @@ class LegacyCablePathTests(CablePathTestCase):
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
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,
+ ),
+ ])
circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit,
termination=self.site,
@@ -1601,22 +1791,44 @@ class LegacyCablePathTests(CablePathTestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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)
- rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
- rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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
- )
- frontport3 = FrontPort.objects.create(
- device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
- )
- frontport4 = FrontPort.objects.create(
- device=self.device, name='Front Port 4', rear_port=rearport4, 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')
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3')
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3')
+ frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4')
+ 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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ ])
# Create cables 1-2
cable1 = Cable(
@@ -1688,30 +1900,72 @@ class LegacyCablePathTests(CablePathTestCase):
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
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
- )
- frontport1_3 = FrontPort.objects.create(
- device=self.device, name='Front Port 1:3', rear_port=rearport1, rear_port_position=3
- )
- frontport1_4 = FrontPort.objects.create(
- device=self.device, name='Front Port 1:4', rear_port=rearport1, rear_port_position=4
- )
- 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
- )
- frontport2_3 = FrontPort.objects.create(
- device=self.device, name='Front Port 2:3', rear_port=rearport2, rear_port_position=3
- )
- frontport2_4 = FrontPort.objects.create(
- device=self.device, name='Front Port 2:4', rear_port=rearport2, rear_port_position=4
- )
+ 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')
+ frontport1_3 = FrontPort.objects.create(device=self.device, name='Front Port 1:3')
+ frontport1_4 = FrontPort.objects.create(device=self.device, name='Front Port 1:4')
+ 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')
+ frontport2_3 = FrontPort.objects.create(device=self.device, name='Front Port 2:3')
+ frontport2_4 = FrontPort.objects.create(device=self.device, name='Front Port 2:4')
+ 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=frontport1_3,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=3,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport1_4,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=4,
+ ),
+ 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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport2_3,
+ front_port_position=1,
+ rear_port=rearport2,
+ rear_port_position=3,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport2_4,
+ front_port_position=1,
+ rear_port=rearport2,
+ rear_port_position=4,
+ ),
+ ])
# Create cables 1-2
cable1 = Cable(
@@ -1858,22 +2112,44 @@ class LegacyCablePathTests(CablePathTestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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)
- rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
- rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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
- )
- frontport3 = FrontPort.objects.create(
- device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
- )
- frontport4 = FrontPort.objects.create(
- device=self.device, name='Front Port 4', rear_port=rearport4, 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')
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3')
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3')
+ frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4')
+ 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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ ])
cable2 = Cable(
a_terminations=[rearport1],
@@ -1937,22 +2213,44 @@ class LegacyCablePathTests(CablePathTestCase):
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
- 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)
- rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
- rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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
- )
- frontport3 = FrontPort.objects.create(
- device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
- )
- frontport4 = FrontPort.objects.create(
- device=self.device, name='Front Port 4', rear_port=rearport4, 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')
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3')
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3')
+ frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4')
+ 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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ ])
cable2 = Cable(
a_terminations=[rearport1],
@@ -2033,30 +2331,62 @@ class LegacyCablePathTests(CablePathTestCase):
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
- 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)
- rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
- rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1)
- rearport5 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1)
- rearport6 = RearPort.objects.create(device=self.device, name='Rear Port 6', 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
- )
- frontport3 = FrontPort.objects.create(
- device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
- )
- frontport4 = FrontPort.objects.create(
- device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1
- )
- frontport5 = FrontPort.objects.create(
- device=self.device, name='Front Port 5', rear_port=rearport5, rear_port_position=1
- )
- frontport6 = FrontPort.objects.create(
- device=self.device, name='Front Port 6', rear_port=rearport6, 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')
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3')
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4')
+ rearport5 = RearPort.objects.create(device=self.device, name='Rear Port 5')
+ rearport6 = RearPort.objects.create(device=self.device, name='Rear Port 6')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3')
+ frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4')
+ frontport5 = FrontPort.objects.create(device=self.device, name='Front Port 5')
+ frontport6 = FrontPort.objects.create(device=self.device, name='Front Port 6')
+ 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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport5,
+ front_port_position=1,
+ rear_port=rearport5,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport6,
+ front_port_position=1,
+ rear_port=rearport6,
+ rear_port_position=1,
+ ),
+ ])
cable2 = Cable(
a_terminations=[rearport1],
@@ -2155,14 +2485,26 @@ class LegacyCablePathTests(CablePathTestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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,
+ ),
+ ])
cable1 = Cable(
a_terminations=[interface1],
@@ -2274,14 +2616,26 @@ class LegacyCablePathTests(CablePathTestCase):
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
- rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
- rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
- frontport1 = FrontPort.objects.create(
- device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
- )
- frontport2 = FrontPort.objects.create(
- device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
- )
+ 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(
@@ -2320,14 +2674,26 @@ class LegacyCablePathTests(CablePathTestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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 cable 2
cable2 = Cable(
@@ -2373,10 +2739,17 @@ class LegacyCablePathTests(CablePathTestCase):
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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 1 and 2
cable1 = Cable(
@@ -2478,22 +2851,44 @@ class LegacyCablePathTests(CablePathTestCase):
)
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
- 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)
- rearport3 = RearPort.objects.create(device=device, name='Rear Port 3', positions=1)
- rearport4 = RearPort.objects.create(device=device, name='Rear Port 4', 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
- )
- frontport3 = FrontPort.objects.create(
- device=device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
- )
- frontport4 = FrontPort.objects.create(
- device=device, name='Front Port 4', rear_port=rearport4, 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')
+ rearport3 = RearPort.objects.create(device=device, name='Rear Port 3')
+ rearport4 = RearPort.objects.create(device=device, name='Rear Port 4')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3')
+ frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4')
+ 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,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport3,
+ front_port_position=1,
+ rear_port=rearport3,
+ rear_port_position=1,
+ ),
+ PortMapping(
+ device=self.device,
+ front_port=frontport4,
+ front_port_position=1,
+ rear_port=rearport4,
+ rear_port_position=1,
+ ),
+ ])
cable2 = Cable(
a_terminations=[rearport1],
diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py
index c7895c6d2..0f9a704c5 100644
--- a/netbox/dcim/tests/test_cablepaths2.py
+++ b/netbox/dcim/tests/test_cablepaths2.py
@@ -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]),
@@ -786,3 +839,205 @@ class CablePathTests(CablePathTestCase):
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
+
+ def test_304_add_port_mapping_between_connected_ports(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
+ """
+ interface1 = Interface.objects.create(device=self.device, name='Interface 1')
+ interface2 = Interface.objects.create(device=self.device, name='Interface 2')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1]
+ )
+ cable1.save()
+ cable2 = Cable(
+ a_terminations=[interface2],
+ b_terminations=[rearport1]
+ )
+ cable2.save()
+
+ # Check for incomplete paths
+ self.assertPathExists(
+ (interface1, cable1, frontport1),
+ is_complete=False,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface2, cable2, rearport1),
+ is_complete=False,
+ is_active=True
+ )
+
+ # Create a PortMapping between frontport1 and rearport1
+ PortMapping.objects.create(
+ device=self.device,
+ front_port=frontport1,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=1,
+ )
+
+ # Check that paths are now complete
+ self.assertPathExists(
+ (interface1, cable1, frontport1, rearport1, cable2, interface2),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface2, cable2, rearport1, frontport1, cable1, interface1),
+ is_complete=True,
+ is_active=True
+ )
+
+ def test_305_delete_port_mapping_between_connected_ports(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
+ """
+ interface1 = Interface.objects.create(device=self.device, name='Interface 1')
+ interface2 = Interface.objects.create(device=self.device, name='Interface 2')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1]
+ )
+ cable1.save()
+ cable2 = Cable(
+ a_terminations=[interface2],
+ b_terminations=[rearport1]
+ )
+ cable2.save()
+ portmapping1 = PortMapping.objects.create(
+ device=self.device,
+ front_port=frontport1,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=1,
+ )
+
+ # Check for complete paths
+ self.assertPathExists(
+ (interface1, cable1, frontport1, rearport1, cable2, interface2),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface2, cable2, rearport1, frontport1, cable1, interface1),
+ is_complete=True,
+ is_active=True
+ )
+
+ # Delete the PortMapping between frontport1 and rearport1
+ portmapping1.delete()
+
+ # Check that paths are no longer complete
+ self.assertPathExists(
+ (interface1, cable1, frontport1),
+ is_complete=False,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface2, cable2, rearport1),
+ is_complete=False,
+ is_active=True
+ )
+
+ def test_306_change_port_mapping_between_connected_ports(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C3-- [IF3]
+ [IF2] --C2-- [FP2] [RP3] --C4-- [IF4]
+ """
+ interface1 = Interface.objects.create(device=self.device, name='Interface 1')
+ interface2 = Interface.objects.create(device=self.device, name='Interface 2')
+ interface3 = Interface.objects.create(device=self.device, name='Interface 3')
+ interface4 = Interface.objects.create(device=self.device, name='Interface 4')
+ frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+ frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
+ rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
+ rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1]
+ )
+ cable1.save()
+ cable2 = Cable(
+ a_terminations=[interface2],
+ b_terminations=[frontport2]
+ )
+ cable2.save()
+ cable3 = Cable(
+ a_terminations=[interface3],
+ b_terminations=[rearport1]
+ )
+ cable3.save()
+ cable4 = Cable(
+ a_terminations=[interface4],
+ b_terminations=[rearport2]
+ )
+ cable4.save()
+ portmapping1 = PortMapping.objects.create(
+ device=self.device,
+ front_port=frontport1,
+ front_port_position=1,
+ rear_port=rearport1,
+ rear_port_position=1,
+ )
+
+ # Verify expected initial paths
+ self.assertPathExists(
+ (interface1, cable1, frontport1, rearport1, cable3, interface3),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface3, cable3, rearport1, frontport1, cable1, interface1),
+ is_complete=True,
+ is_active=True
+ )
+
+ # Delete and replace the PortMapping to connect interface1 to interface4
+ portmapping1.delete()
+ portmapping2 = PortMapping.objects.create(
+ device=self.device,
+ front_port=frontport1,
+ front_port_position=1,
+ rear_port=rearport2,
+ rear_port_position=1,
+ )
+
+ # Verify expected new paths
+ self.assertPathExists(
+ (interface1, cable1, frontport1, rearport2, cable4, interface4),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface4, cable4, rearport2, frontport1, cable1, interface1),
+ is_complete=True,
+ is_active=True
+ )
+
+ # Delete and replace the PortMapping to connect interface2 to interface4
+ portmapping2.delete()
+ PortMapping.objects.create(
+ device=self.device,
+ front_port=frontport2,
+ front_port_position=1,
+ rear_port=rearport2,
+ rear_port_position=1,
+ )
+
+ # Verify expected new paths
+ self.assertPathExists(
+ (interface2, cable2, frontport2, rearport2, cable4, interface4),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (interface4, cable4, rearport2, frontport2, cable2, interface2),
+ is_complete=True,
+ is_active=True
+ )
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 0ba777204..bd44d4f5d 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -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,32 +2043,38 @@ 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,
+ positions=1,
color=ColorChoices.COLOR_RED,
description='foobar1'
),
FrontPortTemplate(
device_type=device_types[1],
name='Front Port 2',
- rear_port=rear_ports[1],
type=PortTypeChoices.TYPE_110_PUNCH,
+ positions=2,
color=ColorChoices.COLOR_GREEN,
description='foobar2'
),
FrontPortTemplate(
device_type=device_types[2],
name='Front Port 3',
- rear_port=rear_ports[2],
type=PortTypeChoices.TYPE_BNC,
+ positions=3,
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']}
@@ -2096,6 +2088,10 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_positions(self):
+ params = {'positions': [1, 2]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class RearPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = RearPortTemplate.objects.all()
@@ -2752,10 +2748,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 +5091,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 +5103,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 +5115,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 +5125,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
name='Front Port 4',
label='D',
type=PortTypeChoices.TYPE_FC,
- rear_port=rear_ports[3],
- rear_port_position=1,
+ positions=2,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
@@ -5141,8 +5135,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
name='Front Port 5',
label='E',
type=PortTypeChoices.TYPE_FC,
- rear_port=rear_ports[4],
- rear_port_position=1,
+ positions=3,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
@@ -5152,14 +5145,21 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
name='Front Port 6',
label='F',
type=PortTypeChoices.TYPE_FC,
- rear_port=rear_ports[5],
- rear_port_position=1,
+ positions=4,
_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()
@@ -5182,6 +5182,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_positions(self):
+ params = {'positions': [2, 3]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -6420,13 +6424,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)
diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py
index fa654f789..a911cbf25 100644
--- a/netbox/dcim/tests/test_forms.py
+++ b/netbox/dcim/tests/test_forms.py
@@ -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)
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index 877af600b..b3eff0920 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -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)
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 6a34df652..52b02d982 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -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 = (
diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py
index 2380fbd0d..ce4a8c8d5 100644
--- a/netbox/dcim/utils.py
+++ b/netbox/dcim/utils.py
@@ -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)
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 463d98179..b78685f5b 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -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):
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
index 88a3456f7..818cea610 100644
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -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)
diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html
index 9f4d23e60..08a3a8d2f 100644
--- a/netbox/templates/dcim/frontport.html
+++ b/netbox/templates/dcim/frontport.html
@@ -47,12 +47,8 @@
- | {% trans "Rear Port" %} |
- {{ object.rear_port|linkify }} |
-
-
- | {% trans "Rear Port Position" %} |
- {{ object.rear_port_position }} |
+ {% trans "Positions" %} |
+ {{ object.positions }} |
| {% trans "Description" %} |
@@ -65,6 +61,18 @@
{% plugin_left_page object %}
+
+
+
+ {% for mapping in rear_port_mappings %}
+
+ | {{ mapping.front_port_position }} |
+ {{ mapping.rear_port|linkify }} |
+ {{ mapping.rear_port_position }} |
+
+ {% endfor %}
+
+
{% if object.mark_connected %}
diff --git a/netbox/templates/dcim/rearport.html b/netbox/templates/dcim/rearport.html
index 3fed4307b..98cb70851 100644
--- a/netbox/templates/dcim/rearport.html
+++ b/netbox/templates/dcim/rearport.html
@@ -61,6 +61,18 @@
{% plugin_left_page object %}
+
+
+
+ {% for mapping in front_port_mappings %}
+
+ | {{ mapping.rear_port_position }} |
+ {{ mapping.front_port|linkify }} |
+ {{ mapping.front_port_position }} |
+
+ {% endfor %}
+
+
{% if object.mark_connected %}
diff --git a/netbox/utilities/relations.py b/netbox/utilities/relations.py
index d5e88299c..3514d394d 100644
--- a/netbox/utilities/relations.py
+++ b/netbox/utilities/relations.py
@@ -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: