This commit is contained in:
Jeremy Stretch 2025-12-08 08:20:05 -05:00 committed by GitHub
commit 651c48f90a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 2487 additions and 939 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -109,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]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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):
"""

View File

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

View File

@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
from dcim.constants import *
from dcim.fields import WWNField
from dcim.models.base import PortMappingBase
from dcim.models.mixins import InterfaceValidationMixin
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, NetBoxModel
@ -35,6 +36,7 @@ __all__ = (
'InventoryItemRole',
'ModuleBay',
'PathEndpoint',
'PortMapping',
'PowerOutlet',
'PowerPort',
'RearPort',
@ -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)
})

View File

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

View File

@ -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):

View File

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

View File

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

View File

@ -973,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()

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
from unittest import skipIf
from circuits.models import CircuitTermination
from dcim.choices import CableProfileChoices
from dcim.models import *
@ -363,13 +361,17 @@ class CablePathTests(CablePathTestCase):
Interface.objects.create(device=self.device, name='Interface 3'),
Interface.objects.create(device=self.device, name='Interface 4'),
]
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
frontport1 = FrontPort.objects.create(
device=self.device,
name='Front Port 1',
rear_port=rearport1,
rear_port_position=1
)
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=frontport1,
front_port_position=1,
rear_port=rearport1,
rear_port_position=1,
),
])
# Create cables
cable1 = Cable(
@ -439,18 +441,40 @@ class CablePathTests(CablePathTestCase):
]
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
frontport1_1 = FrontPort.objects.create(
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
)
frontport1_2 = FrontPort.objects.create(
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
)
frontport2_1 = FrontPort.objects.create(
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
)
frontport2_2 = FrontPort.objects.create(
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
)
frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1')
frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2')
frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1')
frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2')
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=frontport1_1,
front_port_position=1,
rear_port=rearport1,
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=frontport1_2,
front_port_position=1,
rear_port=rearport1,
rear_port_position=2,
),
PortMapping(
device=self.device,
front_port=frontport2_1,
front_port_position=1,
rear_port=rearport2,
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=frontport2_2,
front_port_position=1,
rear_port=rearport2,
rear_port_position=2,
),
])
# Create cables
cable1 = Cable(
@ -654,25 +678,47 @@ class CablePathTests(CablePathTestCase):
Interface.objects.create(device=self.device, name='Interface 2'),
]
rear_ports = [
RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1),
RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1),
RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1),
RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1),
RearPort.objects.create(device=self.device, name='Rear Port 1'),
RearPort.objects.create(device=self.device, name='Rear Port 2'),
RearPort.objects.create(device=self.device, name='Rear Port 3'),
RearPort.objects.create(device=self.device, name='Rear Port 4'),
]
front_ports = [
FrontPort.objects.create(
device=self.device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
),
FrontPort.objects.create(
device=self.device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1
),
FrontPort.objects.create(
device=self.device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1
),
FrontPort.objects.create(
device=self.device, name='Front Port 4', rear_port=rear_ports[3], rear_port_position=1
),
FrontPort.objects.create(device=self.device, name='Front Port 1'),
FrontPort.objects.create(device=self.device, name='Front Port 2'),
FrontPort.objects.create(device=self.device, name='Front Port 3'),
FrontPort.objects.create(device=self.device, name='Front Port 4'),
]
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=front_ports[0],
front_port_position=1,
rear_port=rear_ports[0],
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=front_ports[1],
front_port_position=1,
rear_port=rear_ports[1],
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=front_ports[2],
front_port_position=1,
rear_port=rear_ports[2],
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=front_ports[3],
front_port_position=1,
rear_port=rear_ports[3],
rear_port_position=1,
),
])
# Create cables
cable1 = Cable(
@ -723,8 +769,6 @@ class CablePathTests(CablePathTestCase):
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# TODO: Revisit this test under FR #20564
@skipIf(True, "Waiting for FR #20564")
def test_223_single_path_via_multiple_pass_throughs_with_breakouts(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [IF3]
@ -736,14 +780,26 @@ class CablePathTests(CablePathTestCase):
Interface.objects.create(device=self.device, name='Interface 3'),
Interface.objects.create(device=self.device, name='Interface 4'),
]
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
frontport1 = FrontPort.objects.create(
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
)
frontport2 = FrontPort.objects.create(
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
)
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2')
PortMapping.objects.bulk_create([
PortMapping(
device=self.device,
front_port=frontport1,
front_port_position=1,
rear_port=rearport1,
rear_port_position=1,
),
PortMapping(
device=self.device,
front_port=frontport2,
front_port_position=1,
rear_port=rearport2,
rear_port_position=1,
),
])
# Create cables
cable1 = Cable(
@ -761,9 +817,6 @@ class CablePathTests(CablePathTestCase):
cable2.clean()
cable2.save()
for path in CablePath.objects.all():
print(f'{path}: {path.path_objects}')
# Validate paths
self.assertPathExists(
(interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]),
@ -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
)

View File

@ -1355,22 +1355,15 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create(
(
FrontPortTemplate(
device_type=device_types[0],
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[0],
),
FrontPortTemplate(
device_type=device_types[1],
name='Front Port 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[1],
),
)
front_ports = (
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
])
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
@ -1626,22 +1619,15 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
RearPortTemplate(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create(
(
FrontPortTemplate(
module_type=module_types[0],
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[0],
),
FrontPortTemplate(
module_type=module_types[1],
name='Front Port 2',
type=PortTypeChoices.TYPE_8P8C,
rear_port=rear_ports[1],
),
)
front_ports = (
FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
)
FrontPortTemplate.objects.bulk_create(front_ports)
PortTemplateMapping.objects.bulk_create([
PortTemplateMapping(module_type=module_types[0], front_port=front_ports[0], rear_port=rear_ports[0]),
PortTemplateMapping(module_type=module_types[1], front_port=front_ports[1], rear_port=rear_ports[1]),
])
def test_q(self):
params = {'q': 'foobar1'}
@ -2057,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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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