mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-10 18:39:36 -06:00
Merge 107c1f25c8 into 3483d979d4
This commit is contained in:
commit
651c48f90a
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
#
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
219
netbox/dcim/migrations/0222_port_mappings.py
Normal file
219
netbox/dcim/migrations/0222_port_mappings.py
Normal 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
|
||||
),
|
||||
]
|
||||
65
netbox/dcim/migrations/0223_frontport_positions.py
Normal file
65
netbox/dcim/migrations/0223_frontport_positions.py
Normal 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)
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
61
netbox/dcim/models/base.py
Normal file
61
netbox/dcim/models/base.py
Normal 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
|
||||
)
|
||||
})
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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')),
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
|
||||
@ -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
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user