mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-12 11:29: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 drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConnectedEndpointsSerializer',
|
'ConnectedEndpointsSerializer',
|
||||||
|
'PortSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -35,3 +37,53 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
|||||||
@extend_schema_field(serializers.BooleanField)
|
@extend_schema_field(serializers.BooleanField)
|
||||||
def get_connected_endpoints_reachable(self, obj):
|
def get_connected_endpoints_reachable(self, obj):
|
||||||
return obj._path and obj._path.is_complete and obj._path.is_active
|
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.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortMapping,
|
||||||
RearPort, VirtualDeviceContext,
|
PowerOutlet, PowerPort, RearPort, VirtualDeviceContext,
|
||||||
)
|
)
|
||||||
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.gfk_fields import GFKSerializerField
|
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 vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
|
||||||
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
|
||||||
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
from .base import ConnectedEndpointsSerializer
|
from .base import ConnectedEndpointsSerializer, PortSerializer
|
||||||
from .cables import CabledObjectSerializer
|
from .cables import CabledObjectSerializer
|
||||||
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||||
from .manufacturers import ManufacturerSerializer
|
from .manufacturers import ManufacturerSerializer
|
||||||
@ -294,7 +294,20 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
return super().validate(data)
|
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)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@ -303,28 +316,36 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
|||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
front_ports = RearPortMappingSerializer(
|
||||||
|
source='mappings',
|
||||||
|
many=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
|
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|
||||||
class FrontPortRearPortSerializer(WritableNestedSerializer):
|
class FrontPortMappingSerializer(serializers.ModelSerializer):
|
||||||
"""
|
position = serializers.IntegerField(
|
||||||
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
|
source='front_port_position'
|
||||||
"""
|
)
|
||||||
|
rear_port = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=RearPort.objects.all(),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = PortMapping
|
||||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
|
fields = ('position', 'rear_port', 'rear_port_position')
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
module = ModuleSerializer(
|
module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
@ -333,14 +354,18 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
|||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_port = FrontPortRearPortSerializer()
|
rear_ports = FrontPortMappingSerializer(
|
||||||
|
source='mappings',
|
||||||
|
many=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_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.constants import *
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
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.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.gfk_fields import GFKSerializerField
|
from netbox.api.gfk_fields import GFKSerializerField
|
||||||
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
|
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
|
from .base import PortSerializer
|
||||||
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
|
||||||
from .manufacturers import ManufacturerSerializer
|
from .manufacturers import ManufacturerSerializer
|
||||||
from .nested import NestedInterfaceTemplateSerializer
|
from .nested import NestedInterfaceTemplateSerializer
|
||||||
@ -205,7 +207,20 @@ class InterfaceTemplateSerializer(ComponentTemplateSerializer):
|
|||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
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(
|
device_type = DeviceTypeSerializer(
|
||||||
required=False,
|
required=False,
|
||||||
nested=True,
|
nested=True,
|
||||||
@ -219,17 +234,35 @@ class RearPortTemplateSerializer(ComponentTemplateSerializer):
|
|||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
|
front_ports = RearPortTemplateMappingSerializer(
|
||||||
|
source='mappings',
|
||||||
|
many=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'positions', 'description', 'created', 'last_updated',
|
'front_ports', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
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(
|
device_type = DeviceTypeSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
required=False,
|
required=False,
|
||||||
@ -243,13 +276,17 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer):
|
|||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_port = RearPortTemplateSerializer(nested=True)
|
rear_ports = FrontPortTemplateMappingSerializer(
|
||||||
|
source='mappings',
|
||||||
|
many=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
|
'rear_ports', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
|||||||
@ -32,8 +32,8 @@ CABLE_POSITION_MAX = 1024
|
|||||||
# RearPorts
|
# RearPorts
|
||||||
#
|
#
|
||||||
|
|
||||||
REARPORT_POSITIONS_MIN = 1
|
PORT_POSITION_MIN = 1
|
||||||
REARPORT_POSITIONS_MAX = 1024
|
PORT_POSITION_MAX = 1024
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@ -904,12 +904,15 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
|||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
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:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
|
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||||
|
|
||||||
|
|
||||||
@register_filterset
|
@register_filterset
|
||||||
@ -918,6 +921,12 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
|||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
null_value=None
|
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:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
@ -2137,13 +2146,16 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
|||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
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:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = (
|
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',
|
'cable_position',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2154,6 +2166,12 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
|||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
null_value=None
|
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:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
|
|||||||
@ -1075,12 +1075,6 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
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(
|
type = CSVChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@ -1090,32 +1084,9 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
|
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'owner', 'tags'
|
||||||
'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):
|
class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
|
||||||
device = CSVModelChoiceField(
|
device = CSVModelChoiceField(
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.constants import LOCATION_SCOPE_TYPES
|
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 import get_field_value
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
||||||
@ -13,6 +15,7 @@ from utilities.templatetags.builtins.filters import bettertitle
|
|||||||
from utilities.forms.widgets import HTMXSelect
|
from utilities.forms.widgets import HTMXSelect
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'FrontPortFormMixin',
|
||||||
'ScopedBulkEditForm',
|
'ScopedBulkEditForm',
|
||||||
'ScopedForm',
|
'ScopedForm',
|
||||||
'ScopedImportForm',
|
'ScopedImportForm',
|
||||||
@ -128,3 +131,75 @@ class ScopedImportForm(forms.Form):
|
|||||||
"Please select a {scope_type}."
|
"Please select a {scope_type}."
|
||||||
).format(scope_type=scope_type.model_class()._meta.model_name)
|
).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.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from dcim.forms.mixins import FrontPortFormMixin
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.choices import VLANQinQRoleChoices
|
from ipam.choices import VLANQinQRoleChoices
|
||||||
@ -1112,34 +1113,66 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateForm(ModularComponentTemplateForm):
|
class FrontPortTemplateForm(FrontPortFormMixin, 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',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
TabbedGroups(
|
TabbedGroups(
|
||||||
FieldSet('device_type', name=_('Device Type')),
|
FieldSet('device_type', name=_('Device Type')),
|
||||||
FieldSet('module_type', name=_('Module 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:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
|
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||||
'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):
|
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@ -1578,17 +1611,10 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FrontPortForm(ModularDeviceComponentForm):
|
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||||
rear_port = DynamicModelChoiceField(
|
|
||||||
queryset=RearPort.objects.all(),
|
|
||||||
query_params={
|
|
||||||
'device_id': '$device',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
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',
|
'description', 'tags',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -1596,10 +1622,49 @@ class FrontPortForm(ModularDeviceComponentForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
|
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner',
|
||||||
'description', 'owner', 'tags',
|
'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):
|
class RearPortForm(ModularDeviceComponentForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
|||||||
@ -109,85 +109,30 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
|
|||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
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 = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
TabbedGroups(
|
TabbedGroups(
|
||||||
FieldSet('device_type', name=_('Device Type')),
|
FieldSet('device_type', name=_('Device Type')),
|
||||||
FieldSet('module_type', name=_('Module 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):
|
class Meta:
|
||||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
model = FrontPortTemplate
|
||||||
|
fields = (
|
||||||
def __init__(self, *args, **kwargs):
|
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
|
||||||
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
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
def get_iterative_data(self, iteration):
|
def get_iterative_data(self, iteration):
|
||||||
|
positions = self.cleaned_data['positions']
|
||||||
# Assign rear port and position from selected set
|
offset = positions * iteration
|
||||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rear_port': int(rear_port),
|
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
||||||
'rear_port_position': int(position),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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
|
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
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):
|
class Meta:
|
||||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
model = FrontPort
|
||||||
|
fields = [
|
||||||
def __init__(self, *args, **kwargs):
|
'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags',
|
||||||
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()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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):
|
def get_iterative_data(self, iteration):
|
||||||
|
positions = self.cleaned_data['positions']
|
||||||
# Assign rear port and position from selected set
|
offset = positions * iteration
|
||||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rear_port': int(rear_port),
|
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
||||||
'rear_port_position': int(position),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ __all__ = (
|
|||||||
'InterfaceTemplateImportForm',
|
'InterfaceTemplateImportForm',
|
||||||
'InventoryItemTemplateImportForm',
|
'InventoryItemTemplateImportForm',
|
||||||
'ModuleBayTemplateImportForm',
|
'ModuleBayTemplateImportForm',
|
||||||
|
'PortTemplateMappingImportForm',
|
||||||
'PowerOutletTemplateImportForm',
|
'PowerOutletTemplateImportForm',
|
||||||
'PowerPortTemplateImportForm',
|
'PowerPortTemplateImportForm',
|
||||||
'RearPortTemplateImportForm',
|
'RearPortTemplateImportForm',
|
||||||
@ -113,31 +114,11 @@ class FrontPortTemplateImportForm(forms.ModelForm):
|
|||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
choices=PortTypeChoices.CHOICES
|
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:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label',
|
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
|
||||||
'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 ModuleBayTemplateImportForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import strawberry_django
|
|||||||
from strawberry.scalars import ID
|
from strawberry.scalars import ID
|
||||||
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup
|
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 import models
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.graphql.enums import InterfaceKindEnum
|
from dcim.graphql.enums import InterfaceKindEnum
|
||||||
@ -75,6 +75,8 @@ __all__ = (
|
|||||||
'ModuleTypeFilter',
|
'ModuleTypeFilter',
|
||||||
'ModuleTypeProfileFilter',
|
'ModuleTypeProfileFilter',
|
||||||
'PlatformFilter',
|
'PlatformFilter',
|
||||||
|
'PortMappingFilter',
|
||||||
|
'PortTemplateMappingFilter',
|
||||||
'PowerFeedFilter',
|
'PowerFeedFilter',
|
||||||
'PowerOutletFilter',
|
'PowerOutletFilter',
|
||||||
'PowerOutletTemplateFilter',
|
'PowerOutletTemplateFilter',
|
||||||
@ -409,13 +411,6 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
|||||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||||
strawberry_django.filter_field()
|
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)
|
@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 = (
|
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||||
strawberry_django.filter_field()
|
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 = (
|
rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
rear_port_id: ID | None = strawberry_django.filter_field()
|
front_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||||
rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||||
strawberry_django.filter_field()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||||
|
|||||||
@ -385,7 +385,8 @@ class DeviceTypeType(PrimaryObjectType):
|
|||||||
)
|
)
|
||||||
class FrontPortType(ModularComponentType, CabledObjectMixin):
|
class FrontPortType(ModularComponentType, CabledObjectMixin):
|
||||||
color: str
|
color: str
|
||||||
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
|
|
||||||
|
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -396,7 +397,8 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
|
|||||||
)
|
)
|
||||||
class FrontPortTemplateType(ModularComponentTemplateType):
|
class FrontPortTemplateType(ModularComponentTemplateType):
|
||||||
color: str
|
color: str
|
||||||
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
|
||||||
|
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -636,6 +638,28 @@ class PlatformType(NestedGroupObjectType):
|
|||||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
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(
|
@strawberry_django.type(
|
||||||
models.PowerFeed,
|
models.PowerFeed,
|
||||||
exclude=['_path'],
|
exclude=['_path'],
|
||||||
@ -768,7 +792,7 @@ class RackRoleType(OrganizationalObjectType):
|
|||||||
class RearPortType(ModularComponentType, CabledObjectMixin):
|
class RearPortType(ModularComponentType, CabledObjectMixin):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
|
mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -780,7 +804,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
|
|||||||
class RearPortTemplateType(ModularComponentTemplateType):
|
class RearPortTemplateType(ModularComponentTemplateType):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@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 itertools
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -22,7 +23,7 @@ from utilities.fields import ColorField, GenericArrayForeignKey
|
|||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.serialization import deserialize_object, serialize_object
|
from utilities.serialization import deserialize_object, serialize_object
|
||||||
from wireless.models import WirelessLink
|
from wireless.models import WirelessLink
|
||||||
from .device_components import FrontPort, PathEndpoint, RearPort
|
from .device_components import FrontPort, PathEndpoint, PortMapping, RearPort
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Cable',
|
'Cable',
|
||||||
@ -30,6 +31,8 @@ __all__ = (
|
|||||||
'CableTermination',
|
'CableTermination',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(f'netbox.{__name__}')
|
||||||
|
|
||||||
trace_paths = Signal()
|
trace_paths = Signal()
|
||||||
|
|
||||||
|
|
||||||
@ -666,7 +669,13 @@ class CablePath(models.Model):
|
|||||||
is_active = True
|
is_active = True
|
||||||
is_split = False
|
is_split = False
|
||||||
|
|
||||||
|
logger.debug(f'Tracing cable path from {terminations}...')
|
||||||
|
|
||||||
|
segment = 0
|
||||||
while terminations:
|
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
|
# Terminations must all be of the same type
|
||||||
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
|
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])
|
position_stack.append([terminations[0].cable_position])
|
||||||
|
|
||||||
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
# 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(links) == 0:
|
||||||
if len(path) == 1:
|
if len(path) == 1:
|
||||||
# If this is the start of the path and no link exists, return None
|
# 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
|
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
|
# 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:]):
|
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
|
||||||
is_complete = False
|
is_complete = False
|
||||||
is_split = True
|
is_split = True
|
||||||
|
logger.debug('Remote termination types differ; aborting trace.')
|
||||||
break
|
break
|
||||||
|
|
||||||
# Step 7: Record the far-end termination object(s)
|
# Step 7: Record the far-end termination object(s)
|
||||||
@ -777,58 +792,53 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
if isinstance(remote_terminations[0], FrontPort):
|
if isinstance(remote_terminations[0], FrontPort):
|
||||||
# Follow FrontPorts to their corresponding RearPorts
|
# Follow FrontPorts to their corresponding RearPorts
|
||||||
rear_ports = RearPort.objects.filter(
|
if remote_terminations[0].positions > 1 and position_stack:
|
||||||
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:
|
|
||||||
positions = position_stack.pop()
|
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()
|
q_filter = Q()
|
||||||
for rt in remote_terminations:
|
for rt in remote_terminations:
|
||||||
position = positions.pop()
|
q_filter |= Q(front_port=rt, front_port_position__in=positions)
|
||||||
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
|
port_mappings = PortMapping.objects.filter(q_filter)
|
||||||
if q_filter is Q():
|
elif remote_terminations[0].positions > 1:
|
||||||
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
|
|
||||||
is_split = True
|
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
|
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):
|
elif isinstance(remote_terminations[0], CircuitTermination):
|
||||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
# 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
|
# Unsupported topology, mark as split and exit
|
||||||
is_complete = False
|
is_complete = False
|
||||||
is_split = True
|
is_split = True
|
||||||
|
logger.warning('Encountered an unsupported topology; aborting trace.')
|
||||||
break
|
break
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
@ -954,16 +965,23 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
# RearPort splitting to multiple FrontPorts with no stack position
|
# RearPort splitting to multiple FrontPorts with no stack position
|
||||||
if type(nodes[0]) is RearPort:
|
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
|
# Cable terminating to multiple FrontPorts mapped to different
|
||||||
# RearPorts connected to different cables
|
# RearPorts connected to different cables
|
||||||
elif type(nodes[0]) is FrontPort:
|
if type(nodes[0]) is FrontPort:
|
||||||
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
return [
|
||||||
|
mapping.rear_port for mapping in
|
||||||
|
PortMapping.objects.filter(front_port__in=nodes).prefetch_related('rear_port')
|
||||||
|
]
|
||||||
# Cable terminating to multiple CircuitTerminations
|
# Cable terminating to multiple CircuitTerminations
|
||||||
elif type(nodes[0]) is CircuitTermination:
|
if type(nodes[0]) is CircuitTermination:
|
||||||
return [
|
return [
|
||||||
ct.get_peer_termination() for ct in nodes
|
ct.get_peer_termination() for ct in nodes
|
||||||
]
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
def get_asymmetric_nodes(self):
|
def get_asymmetric_nodes(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from dcim.models.base import PortMappingBase
|
||||||
from dcim.models.mixins import InterfaceValidationMixin
|
from dcim.models.mixins import InterfaceValidationMixin
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@ -28,6 +29,7 @@ __all__ = (
|
|||||||
'InterfaceTemplate',
|
'InterfaceTemplate',
|
||||||
'InventoryItemTemplate',
|
'InventoryItemTemplate',
|
||||||
'ModuleBayTemplate',
|
'ModuleBayTemplate',
|
||||||
|
'PortTemplateMapping',
|
||||||
'PowerOutletTemplate',
|
'PowerOutletTemplate',
|
||||||
'PowerPortTemplate',
|
'PowerPortTemplate',
|
||||||
'RearPortTemplate',
|
'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):
|
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
Template for a pass-through port on the front of a new Device.
|
Template for a pass-through port on the front of a new Device.
|
||||||
@ -531,18 +580,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name=_('color'),
|
verbose_name=_('color'),
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
rear_port = models.ForeignKey(
|
positions = models.PositiveSmallIntegerField(
|
||||||
to='dcim.RearPortTemplate',
|
verbose_name=_('positions'),
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='frontport_templates'
|
|
||||||
)
|
|
||||||
rear_port_position = models.PositiveSmallIntegerField(
|
|
||||||
verbose_name=_('rear port position'),
|
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
MinValueValidator(PORT_POSITION_MIN),
|
||||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
MaxValueValidator(PORT_POSITION_MAX)
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
component_model = FrontPort
|
component_model = FrontPort
|
||||||
@ -557,10 +601,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
fields=('module_type', 'name'),
|
fields=('module_type', 'name'),
|
||||||
name='%(app_label)s_%(class)s_unique_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 = _('front port template')
|
||||||
verbose_name_plural = _('front port templates')
|
verbose_name_plural = _('front port templates')
|
||||||
@ -568,40 +608,23 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
try:
|
# Check that positions is greater than or equal to the number of associated RearPortTemplates
|
||||||
|
if not self._state.adding:
|
||||||
# Validate rear port assignment
|
mapping_count = self.mappings.count()
|
||||||
if self.rear_port.device_type != self.device_type:
|
if self.positions < mapping_count:
|
||||||
raise ValidationError(
|
raise ValidationError({
|
||||||
_("Rear port ({name}) must belong to the same device type").format(name=self.rear_port)
|
"positions": _(
|
||||||
)
|
"The number of positions cannot be less than the number of mapped rear port templates ({count})"
|
||||||
|
).format(count=mapping_count)
|
||||||
# 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
|
|
||||||
|
|
||||||
def instantiate(self, **kwargs):
|
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(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(kwargs.get('module')),
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
rear_port=rear_port,
|
positions=self.positions,
|
||||||
rear_port_position=self.rear_port_position,
|
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
instantiate.do_not_call_in_templates = True
|
instantiate.do_not_call_in_templates = True
|
||||||
@ -611,8 +634,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
'color': self.color,
|
'color': self.color,
|
||||||
'rear_port': self.rear_port.name,
|
'positions': self.positions,
|
||||||
'rear_port_position': self.rear_port_position,
|
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
}
|
}
|
||||||
@ -635,9 +657,9 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name=_('positions'),
|
verbose_name=_('positions'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
MinValueValidator(PORT_POSITION_MIN),
|
||||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
MaxValueValidator(PORT_POSITION_MAX)
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
component_model = RearPort
|
component_model = RearPort
|
||||||
@ -646,6 +668,20 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name = _('rear port template')
|
verbose_name = _('rear port template')
|
||||||
verbose_name_plural = _('rear port templates')
|
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):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(kwargs.get('module')),
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import WWNField
|
from dcim.fields import WWNField
|
||||||
|
from dcim.models.base import PortMappingBase
|
||||||
from dcim.models.mixins import InterfaceValidationMixin
|
from dcim.models.mixins import InterfaceValidationMixin
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from netbox.models import OrganizationalModel, NetBoxModel
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
@ -35,6 +36,7 @@ __all__ = (
|
|||||||
'InventoryItemRole',
|
'InventoryItemRole',
|
||||||
'ModuleBay',
|
'ModuleBay',
|
||||||
'PathEndpoint',
|
'PathEndpoint',
|
||||||
|
'PortMapping',
|
||||||
'PowerOutlet',
|
'PowerOutlet',
|
||||||
'PowerPort',
|
'PowerPort',
|
||||||
'RearPort',
|
'RearPort',
|
||||||
@ -208,10 +210,6 @@ class CabledObjectModel(models.Model):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
"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:
|
if self.cable_end and not self.cable:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"cable_end": _("Cable end must not be set without a cable.")
|
"cable_end": _("Cable end must not be set without a cable.")
|
||||||
@ -1069,6 +1067,43 @@ class Interface(
|
|||||||
# Pass-through ports
|
# 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):
|
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
@ -1082,22 +1117,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
verbose_name=_('color'),
|
verbose_name=_('color'),
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
rear_port = models.ForeignKey(
|
positions = models.PositiveSmallIntegerField(
|
||||||
to='dcim.RearPort',
|
verbose_name=_('positions'),
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='frontports'
|
|
||||||
)
|
|
||||||
rear_port_position = models.PositiveSmallIntegerField(
|
|
||||||
verbose_name=_('rear port position'),
|
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
MinValueValidator(PORT_POSITION_MIN),
|
||||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
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):
|
class Meta(ModularComponentModel.Meta):
|
||||||
constraints = (
|
constraints = (
|
||||||
@ -1105,10 +1134,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
fields=('device', 'name'),
|
fields=('device', 'name'),
|
||||||
name='%(app_label)s_%(class)s_unique_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 = _('front port')
|
||||||
verbose_name_plural = _('front ports')
|
verbose_name_plural = _('front ports')
|
||||||
@ -1116,27 +1141,14 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if hasattr(self, 'rear_port'):
|
# Check that positions is greater than or equal to the number of associated RearPorts
|
||||||
|
if not self._state.adding:
|
||||||
# Validate rear port assignment
|
mapping_count = self.mappings.count()
|
||||||
if self.rear_port.device != self.device:
|
if self.positions < mapping_count:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"rear_port": _(
|
"positions": _(
|
||||||
"Rear port ({rear_port}) must belong to the same device"
|
"The number of positions cannot be less than the number of mapped rear ports ({count})"
|
||||||
).format(rear_port=self.rear_port)
|
).format(count=mapping_count)
|
||||||
})
|
|
||||||
|
|
||||||
# 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
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1157,11 +1169,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
verbose_name=_('positions'),
|
verbose_name=_('positions'),
|
||||||
default=1,
|
default=1,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
MinValueValidator(PORT_POSITION_MIN),
|
||||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
MaxValueValidator(PORT_POSITION_MAX)
|
||||||
],
|
],
|
||||||
help_text=_('Number of front ports which may be mapped')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('device', 'type', 'color', 'positions')
|
clone_fields = ('device', 'type', 'color', 'positions')
|
||||||
|
|
||||||
class Meta(ModularComponentModel.Meta):
|
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
|
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||||
if not self._state.adding:
|
if not self._state.adding:
|
||||||
frontport_count = self.frontports.count()
|
mapping_count = self.mappings.count()
|
||||||
if self.positions < frontport_count:
|
if self.positions < mapping_count:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"positions": _(
|
"positions": _(
|
||||||
"The number of positions cannot be less than the number of mapped front ports "
|
"The number of positions cannot be less than the number of mapped front ports "
|
||||||
"({frontport_count})"
|
"({count})"
|
||||||
).format(frontport_count=frontport_count)
|
).format(count=mapping_count)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import decimal
|
import decimal
|
||||||
import yaml
|
|
||||||
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
import yaml
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -19,14 +18,14 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import MACAddressField
|
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.models import ConfigContextModel, CustomField
|
||||||
from extras.querysets import ConfigContextModelQuerySet
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||||
from netbox.models.mixins import WeightMixin
|
|
||||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||||
|
from netbox.models.mixins import WeightMixin
|
||||||
from utilities.fields import ColorField, CounterCacheField
|
from utilities.fields import ColorField, CounterCacheField
|
||||||
from utilities.prefetch import get_prefetchable_fields
|
from utilities.prefetch import get_prefetchable_fields
|
||||||
from utilities.tracking import TrackingModelMixin
|
from utilities.tracking import TrackingModelMixin
|
||||||
@ -34,7 +33,6 @@ from .device_components import *
|
|||||||
from .mixins import RenderConfigMixin
|
from .mixins import RenderConfigMixin
|
||||||
from .modules import Module
|
from .modules import Module
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Device',
|
'Device',
|
||||||
'DeviceRole',
|
'DeviceRole',
|
||||||
@ -1003,6 +1001,8 @@ class Device(
|
|||||||
self._instantiate_components(self.device_type.interfacetemplates.all())
|
self._instantiate_components(self.device_type.interfacetemplates.all())
|
||||||
self._instantiate_components(self.device_type.rearporttemplates.all())
|
self._instantiate_components(self.device_type.rearporttemplates.all())
|
||||||
self._instantiate_components(self.device_type.frontporttemplates.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
|
# Disable bulk_create to accommodate MPTT
|
||||||
self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
|
self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
|
||||||
self._instantiate_components(self.device_type.devicebaytemplates.all())
|
self._instantiate_components(self.device_type.devicebaytemplates.all())
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ from dcim.choices import CableEndChoices, LinkStatusChoices
|
|||||||
from virtualization.models import VMInterface
|
from virtualization.models import VMInterface
|
||||||
from .models import (
|
from .models import (
|
||||||
Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
|
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,
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
from .models.cables import trace_paths
|
from .models.cables import trace_paths
|
||||||
@ -135,6 +136,17 @@ def retrace_cable_paths(instance, **kwargs):
|
|||||||
cablepath.retrace()
|
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)
|
@receiver(post_delete, sender=CableTermination)
|
||||||
def nullify_connected_endpoints(instance, **kwargs):
|
def nullify_connected_endpoints(instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -150,17 +162,6 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
cablepath.retrace()
|
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=Interface)
|
||||||
@receiver(post_save, sender=VMInterface)
|
@receiver(post_save, sender=VMInterface)
|
||||||
def update_mac_address_interface(instance, created, raw, **kwargs):
|
def update_mac_address_interface(instance, created, raw, **kwargs):
|
||||||
|
|||||||
@ -749,12 +749,9 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
color = columns.ColorColumn(
|
color = columns.ColorColumn(
|
||||||
verbose_name=_('Color'),
|
verbose_name=_('Color'),
|
||||||
)
|
)
|
||||||
rear_port_position = tables.Column(
|
mappings = columns.ManyToManyColumn(
|
||||||
verbose_name=_('Position')
|
verbose_name=_('Mappings'),
|
||||||
)
|
transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}'
|
||||||
rear_port = tables.Column(
|
|
||||||
verbose_name=_('Rear Port'),
|
|
||||||
linkify=True
|
|
||||||
)
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:frontport_list'
|
url_name='dcim:frontport_list'
|
||||||
@ -763,12 +760,12 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.FrontPort
|
model = models.FrontPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer',
|
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created',
|
||||||
'inventory_items', 'tags', 'created', 'last_updated',
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
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):
|
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
|
||||||
model = models.FrontPort
|
model = models.FrontPort
|
||||||
fields = (
|
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',
|
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
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(
|
color = columns.ColorColumn(
|
||||||
verbose_name=_('Color'),
|
verbose_name=_('Color'),
|
||||||
)
|
)
|
||||||
|
mappings = columns.ManyToManyColumn(
|
||||||
|
verbose_name=_('Mappings'),
|
||||||
|
transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}'
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rearport_list'
|
url_name='dcim:rearport_list'
|
||||||
)
|
)
|
||||||
@ -812,10 +813,13 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
|
|||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.RearPort
|
model = models.RearPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 'last_updated',
|
'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):
|
class DeviceRearPortTable(RearPortTable):
|
||||||
@ -832,11 +836,11 @@ class DeviceRearPortTable(RearPortTable):
|
|||||||
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
|
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
|
||||||
model = models.RearPort
|
model = models.RearPort
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
|
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings',
|
||||||
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
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):
|
class FrontPortTemplateTable(ComponentTemplateTable):
|
||||||
rear_port_position = tables.Column(
|
|
||||||
verbose_name=_('Position')
|
|
||||||
)
|
|
||||||
color = columns.ColorColumn(
|
color = columns.ColorColumn(
|
||||||
verbose_name=_('Color'),
|
verbose_name=_('Color'),
|
||||||
)
|
)
|
||||||
|
mappings = columns.ManyToManyColumn(
|
||||||
|
verbose_name=_('Mappings'),
|
||||||
|
transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}'
|
||||||
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
actions=('edit', 'delete'),
|
actions=('edit', 'delete'),
|
||||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
@ -262,7 +263,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
model = models.FrontPortTemplate
|
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"
|
empty_text = "None"
|
||||||
|
|
||||||
|
|
||||||
@ -270,6 +271,10 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||||||
color = columns.ColorColumn(
|
color = columns.ColorColumn(
|
||||||
verbose_name=_('Color'),
|
verbose_name=_('Color'),
|
||||||
)
|
)
|
||||||
|
mappings = columns.ManyToManyColumn(
|
||||||
|
verbose_name=_('Mappings'),
|
||||||
|
transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}'
|
||||||
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
actions=('edit', 'delete'),
|
actions=('edit', 'delete'),
|
||||||
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
||||||
@ -277,7 +282,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
class Meta(ComponentTemplateTable.Meta):
|
class Meta(ComponentTemplateTable.Meta):
|
||||||
model = models.RearPortTemplate
|
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"
|
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 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 3', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', 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(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPortTemplate(module_type=moduletype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C),
|
RearPortTemplate(device_type=devicetype, 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.objects.bulk_create(rear_port_templates)
|
RearPortTemplate.objects.bulk_create(rear_port_templates)
|
||||||
|
|
||||||
front_port_templates = (
|
front_port_templates = (
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
device_type=devicetype,
|
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
name='Front Port Template 1',
|
FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C),
|
||||||
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.objects.bulk_create(front_port_templates)
|
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 = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Front Port Template 3',
|
'name': 'Front Port Template 3',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_port_templates[2].pk,
|
'rear_ports': [
|
||||||
'rear_port_position': 1,
|
{
|
||||||
|
'position': 1,
|
||||||
|
'rear_port': rear_port_templates[3].pk,
|
||||||
|
'rear_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Front Port Template 4',
|
'name': 'Front Port Template 4',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_port_templates[3].pk,
|
'rear_ports': [
|
||||||
'rear_port_position': 1,
|
{
|
||||||
|
'position': 1,
|
||||||
|
'rear_port': rear_port_templates[4].pk,
|
||||||
|
'rear_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'module_type': moduletype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Front Port Template 7',
|
'name': 'Front Port Template 7',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_port_templates[6].pk,
|
'rear_ports': [
|
||||||
'rear_port_position': 1,
|
{
|
||||||
},
|
'position': 1,
|
||||||
{
|
'rear_port': rear_port_templates[5].pk,
|
||||||
'module_type': moduletype.pk,
|
'rear_port_position': 1,
|
||||||
'name': 'Front Port Template 8',
|
},
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
],
|
||||||
'rear_port': rear_port_templates[7].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):
|
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
@ -1057,36 +1084,104 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
manufacturer=manufacturer, model='Module Type 1'
|
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 = (
|
rear_port_templates = (
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C),
|
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 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 3', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_port_templates)
|
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 = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Rear Port Template 4',
|
'name': 'Rear Port Template 4',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
|
'front_ports': [
|
||||||
|
{
|
||||||
|
'position': 1,
|
||||||
|
'front_port': front_port_templates[3].pk,
|
||||||
|
'front_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Rear Port Template 5',
|
'name': 'Rear Port Template 5',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
|
'front_ports': [
|
||||||
|
{
|
||||||
|
'position': 1,
|
||||||
|
'front_port': front_port_templates[4].pk,
|
||||||
|
'front_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'module_type': moduletype.pk,
|
'module_type': moduletype.pk,
|
||||||
'name': 'Rear Port Template 6',
|
'name': 'Rear Port Template 6',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
},
|
'front_ports': [
|
||||||
{
|
{
|
||||||
'module_type': moduletype.pk,
|
'position': 1,
|
||||||
'name': 'Rear Port Template 7',
|
'front_port': front_port_templates[5].pk,
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'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):
|
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ModuleBayTemplate
|
model = ModuleBayTemplate
|
||||||
@ -2015,51 +2110,90 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
|||||||
RearPort(device=device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C),
|
RearPort(device=device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
RearPort.objects.bulk_create(rear_ports)
|
RearPort.objects.bulk_create(rear_ports)
|
||||||
|
|
||||||
front_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 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
|
FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2]),
|
FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
FrontPort.objects.bulk_create(front_ports)
|
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 = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Front Port 4',
|
'name': 'Front Port 4',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_ports[3].pk,
|
'rear_ports': [
|
||||||
'rear_port_position': 1,
|
{
|
||||||
|
'position': 1,
|
||||||
|
'rear_port': rear_ports[3].pk,
|
||||||
|
'rear_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Front Port 5',
|
'name': 'Front Port 5',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_ports[4].pk,
|
'rear_ports': [
|
||||||
'rear_port_position': 1,
|
{
|
||||||
|
'position': 1,
|
||||||
|
'rear_port': rear_ports[4].pk,
|
||||||
|
'rear_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Front Port 6',
|
'name': 'Front Port 6',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rear_ports[5].pk,
|
'rear_ports': [
|
||||||
'rear_port_position': 1,
|
{
|
||||||
|
'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
|
@tag('regression') # Issue #18991
|
||||||
def test_front_port_paths(self):
|
def test_front_port_paths(self):
|
||||||
device = Device.objects.first()
|
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')
|
interface1 = Interface.objects.create(device=device, name='Interface 1')
|
||||||
front_port = FrontPort.objects.create(
|
rear_port = RearPort.objects.create(device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C)
|
||||||
device=device,
|
front_port = FrontPort.objects.create(device=device, name='Front Port 10', type=PortTypeChoices.TYPE_8P8C)
|
||||||
name='Rear Port 10',
|
PortMapping.objects.create(device=device, front_port=front_port, rear_port=rear_port)
|
||||||
type=PortTypeChoices.TYPE_8P8C,
|
|
||||||
rear_port=rear_port,
|
|
||||||
)
|
|
||||||
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
|
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
|
||||||
|
|
||||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
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')
|
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)
|
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 = (
|
rear_ports = (
|
||||||
RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
|
RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPort(device=device, name='Rear Port 2', 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,
|
'device': device.pk,
|
||||||
'name': 'Rear Port 4',
|
'name': 'Rear Port 4',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
|
'front_ports': [
|
||||||
|
{
|
||||||
|
'position': 1,
|
||||||
|
'front_port': front_ports[3].pk,
|
||||||
|
'front_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Rear Port 5',
|
'name': 'Rear Port 5',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
|
'front_ports': [
|
||||||
|
{
|
||||||
|
'position': 1,
|
||||||
|
'front_port': front_ports[4].pk,
|
||||||
|
'front_port_position': 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Rear Port 6',
|
'name': 'Rear Port 6',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'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
|
@tag('regression') # Issue #18991
|
||||||
def test_rear_port_paths(self):
|
def test_rear_port_paths(self):
|
||||||
device = Device.objects.first()
|
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 circuits.models import CircuitTermination
|
||||||
from dcim.choices import CableProfileChoices
|
from dcim.choices import CableProfileChoices
|
||||||
from dcim.models import *
|
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 3'),
|
||||||
Interface.objects.create(device=self.device, name='Interface 4'),
|
Interface.objects.create(device=self.device, name='Interface 4'),
|
||||||
]
|
]
|
||||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||||
frontport1 = FrontPort.objects.create(
|
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||||
device=self.device,
|
PortMapping.objects.bulk_create([
|
||||||
name='Front Port 1',
|
PortMapping(
|
||||||
rear_port=rearport1,
|
device=self.device,
|
||||||
rear_port_position=1
|
front_port=frontport1,
|
||||||
)
|
front_port_position=1,
|
||||||
|
rear_port=rearport1,
|
||||||
|
rear_port_position=1,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
# Create cables
|
# Create cables
|
||||||
cable1 = Cable(
|
cable1 = Cable(
|
||||||
@ -439,18 +441,40 @@ class CablePathTests(CablePathTestCase):
|
|||||||
]
|
]
|
||||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
|
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
|
||||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
|
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
|
||||||
frontport1_1 = FrontPort.objects.create(
|
frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1')
|
||||||
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')
|
||||||
)
|
frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1')
|
||||||
frontport1_2 = FrontPort.objects.create(
|
frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2')
|
||||||
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
|
PortMapping.objects.bulk_create([
|
||||||
)
|
PortMapping(
|
||||||
frontport2_1 = FrontPort.objects.create(
|
device=self.device,
|
||||||
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
|
front_port=frontport1_1,
|
||||||
)
|
front_port_position=1,
|
||||||
frontport2_2 = FrontPort.objects.create(
|
rear_port=rearport1,
|
||||||
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
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
|
# Create cables
|
||||||
cable1 = Cable(
|
cable1 = Cable(
|
||||||
@ -654,25 +678,47 @@ class CablePathTests(CablePathTestCase):
|
|||||||
Interface.objects.create(device=self.device, name='Interface 2'),
|
Interface.objects.create(device=self.device, name='Interface 2'),
|
||||||
]
|
]
|
||||||
rear_ports = [
|
rear_ports = [
|
||||||
RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1),
|
RearPort.objects.create(device=self.device, name='Rear Port 1'),
|
||||||
RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1),
|
RearPort.objects.create(device=self.device, name='Rear Port 2'),
|
||||||
RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1),
|
RearPort.objects.create(device=self.device, name='Rear Port 3'),
|
||||||
RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1),
|
RearPort.objects.create(device=self.device, name='Rear Port 4'),
|
||||||
]
|
]
|
||||||
front_ports = [
|
front_ports = [
|
||||||
FrontPort.objects.create(
|
FrontPort.objects.create(device=self.device, name='Front Port 1'),
|
||||||
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'),
|
||||||
),
|
FrontPort.objects.create(device=self.device, name='Front Port 3'),
|
||||||
FrontPort.objects.create(
|
FrontPort.objects.create(device=self.device, name='Front Port 4'),
|
||||||
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
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
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
|
# Create cables
|
||||||
cable1 = Cable(
|
cable1 = Cable(
|
||||||
@ -723,8 +769,6 @@ class CablePathTests(CablePathTestCase):
|
|||||||
# Test SVG generation
|
# Test SVG generation
|
||||||
CableTraceSVG(interfaces[0]).render()
|
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):
|
def test_223_single_path_via_multiple_pass_throughs_with_breakouts(self):
|
||||||
"""
|
"""
|
||||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF3]
|
[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 3'),
|
||||||
Interface.objects.create(device=self.device, name='Interface 4'),
|
Interface.objects.create(device=self.device, name='Interface 4'),
|
||||||
]
|
]
|
||||||
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
|
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
|
||||||
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
|
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2')
|
||||||
frontport1 = FrontPort.objects.create(
|
frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
|
||||||
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')
|
||||||
)
|
PortMapping.objects.bulk_create([
|
||||||
frontport2 = FrontPort.objects.create(
|
PortMapping(
|
||||||
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
|
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
|
# Create cables
|
||||||
cable1 = Cable(
|
cable1 = Cable(
|
||||||
@ -761,9 +817,6 @@ class CablePathTests(CablePathTestCase):
|
|||||||
cable2.clean()
|
cable2.clean()
|
||||||
cable2.save()
|
cable2.save()
|
||||||
|
|
||||||
for path in CablePath.objects.all():
|
|
||||||
print(f'{path}: {path.path_objects}')
|
|
||||||
|
|
||||||
# Validate paths
|
# Validate paths
|
||||||
self.assertPathExists(
|
self.assertPathExists(
|
||||||
(interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]),
|
(interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]),
|
||||||
@ -786,3 +839,205 @@ class CablePathTests(CablePathTestCase):
|
|||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
self.assertEqual(CablePath.objects.count(), 4)
|
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(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
FrontPortTemplate.objects.bulk_create(
|
front_ports = (
|
||||||
(
|
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
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],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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.objects.bulk_create((
|
||||||
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
|
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
|
||||||
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
|
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(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
FrontPortTemplate.objects.bulk_create(
|
front_ports = (
|
||||||
(
|
FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
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],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -2057,32 +2043,38 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
|
|||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
|
|
||||||
FrontPortTemplate.objects.bulk_create((
|
front_ports = (
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(
|
||||||
device_type=device_types[0],
|
device_type=device_types[0],
|
||||||
name='Front Port 1',
|
name='Front Port 1',
|
||||||
rear_port=rear_ports[0],
|
|
||||||
type=PortTypeChoices.TYPE_8P8C,
|
type=PortTypeChoices.TYPE_8P8C,
|
||||||
|
positions=1,
|
||||||
color=ColorChoices.COLOR_RED,
|
color=ColorChoices.COLOR_RED,
|
||||||
description='foobar1'
|
description='foobar1'
|
||||||
),
|
),
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(
|
||||||
device_type=device_types[1],
|
device_type=device_types[1],
|
||||||
name='Front Port 2',
|
name='Front Port 2',
|
||||||
rear_port=rear_ports[1],
|
|
||||||
type=PortTypeChoices.TYPE_110_PUNCH,
|
type=PortTypeChoices.TYPE_110_PUNCH,
|
||||||
|
positions=2,
|
||||||
color=ColorChoices.COLOR_GREEN,
|
color=ColorChoices.COLOR_GREEN,
|
||||||
description='foobar2'
|
description='foobar2'
|
||||||
),
|
),
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(
|
||||||
device_type=device_types[2],
|
device_type=device_types[2],
|
||||||
name='Front Port 3',
|
name='Front Port 3',
|
||||||
rear_port=rear_ports[2],
|
|
||||||
type=PortTypeChoices.TYPE_BNC,
|
type=PortTypeChoices.TYPE_BNC,
|
||||||
|
positions=3,
|
||||||
color=ColorChoices.COLOR_BLUE,
|
color=ColorChoices.COLOR_BLUE,
|
||||||
description='foobar3'
|
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):
|
def test_name(self):
|
||||||
params = {'name': ['Front Port 1', 'Front Port 2']}
|
params = {'name': ['Front Port 1', 'Front Port 2']}
|
||||||
@ -2096,6 +2088,10 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
|
|||||||
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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):
|
class RearPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = RearPortTemplate.objects.all()
|
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(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
)
|
)
|
||||||
RearPort.objects.bulk_create(rear_ports)
|
RearPort.objects.bulk_create(rear_ports)
|
||||||
FrontPort.objects.bulk_create((
|
front_ports = (
|
||||||
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
|
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
|
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[0], name='Module Bay 1')
|
||||||
ModuleBay.objects.create(device=devices[1], name='Module Bay 2')
|
ModuleBay.objects.create(device=devices[1], name='Module Bay 2')
|
||||||
DeviceBay.objects.bulk_create((
|
DeviceBay.objects.bulk_create((
|
||||||
@ -5090,8 +5091,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
label='A',
|
label='A',
|
||||||
type=PortTypeChoices.TYPE_8P8C,
|
type=PortTypeChoices.TYPE_8P8C,
|
||||||
color=ColorChoices.COLOR_RED,
|
color=ColorChoices.COLOR_RED,
|
||||||
rear_port=rear_ports[0],
|
|
||||||
rear_port_position=1,
|
|
||||||
description='First',
|
description='First',
|
||||||
_site=devices[0].site,
|
_site=devices[0].site,
|
||||||
_location=devices[0].location,
|
_location=devices[0].location,
|
||||||
@ -5104,8 +5103,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
label='B',
|
label='B',
|
||||||
type=PortTypeChoices.TYPE_110_PUNCH,
|
type=PortTypeChoices.TYPE_110_PUNCH,
|
||||||
color=ColorChoices.COLOR_GREEN,
|
color=ColorChoices.COLOR_GREEN,
|
||||||
rear_port=rear_ports[1],
|
|
||||||
rear_port_position=2,
|
|
||||||
description='Second',
|
description='Second',
|
||||||
_site=devices[1].site,
|
_site=devices[1].site,
|
||||||
_location=devices[1].location,
|
_location=devices[1].location,
|
||||||
@ -5118,8 +5115,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
label='C',
|
label='C',
|
||||||
type=PortTypeChoices.TYPE_BNC,
|
type=PortTypeChoices.TYPE_BNC,
|
||||||
color=ColorChoices.COLOR_BLUE,
|
color=ColorChoices.COLOR_BLUE,
|
||||||
rear_port=rear_ports[2],
|
|
||||||
rear_port_position=3,
|
|
||||||
description='Third',
|
description='Third',
|
||||||
_site=devices[2].site,
|
_site=devices[2].site,
|
||||||
_location=devices[2].location,
|
_location=devices[2].location,
|
||||||
@ -5130,8 +5125,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
name='Front Port 4',
|
name='Front Port 4',
|
||||||
label='D',
|
label='D',
|
||||||
type=PortTypeChoices.TYPE_FC,
|
type=PortTypeChoices.TYPE_FC,
|
||||||
rear_port=rear_ports[3],
|
positions=2,
|
||||||
rear_port_position=1,
|
|
||||||
_site=devices[3].site,
|
_site=devices[3].site,
|
||||||
_location=devices[3].location,
|
_location=devices[3].location,
|
||||||
_rack=devices[3].rack,
|
_rack=devices[3].rack,
|
||||||
@ -5141,8 +5135,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
name='Front Port 5',
|
name='Front Port 5',
|
||||||
label='E',
|
label='E',
|
||||||
type=PortTypeChoices.TYPE_FC,
|
type=PortTypeChoices.TYPE_FC,
|
||||||
rear_port=rear_ports[4],
|
positions=3,
|
||||||
rear_port_position=1,
|
|
||||||
_site=devices[3].site,
|
_site=devices[3].site,
|
||||||
_location=devices[3].location,
|
_location=devices[3].location,
|
||||||
_rack=devices[3].rack,
|
_rack=devices[3].rack,
|
||||||
@ -5152,14 +5145,21 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
name='Front Port 6',
|
name='Front Port 6',
|
||||||
label='F',
|
label='F',
|
||||||
type=PortTypeChoices.TYPE_FC,
|
type=PortTypeChoices.TYPE_FC,
|
||||||
rear_port=rear_ports[5],
|
positions=4,
|
||||||
rear_port_position=1,
|
|
||||||
_site=devices[3].site,
|
_site=devices[3].site,
|
||||||
_location=devices[3].location,
|
_location=devices[3].location,
|
||||||
_rack=devices[3].rack,
|
_rack=devices[3].rack,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
FrontPort.objects.bulk_create(front_ports)
|
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
|
# Cables
|
||||||
Cable(a_terminations=[front_ports[0]], b_terminations=[front_ports[3]]).save()
|
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]}
|
params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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):
|
def test_description(self):
|
||||||
params = {'description': ['First', 'Second']}
|
params = {'description': ['First', 'Second']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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')
|
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_port = PowerPort.objects.create(device=devices[0], name='Power Port 1')
|
||||||
power_outlet = PowerOutlet.objects.create(device=devices[0], name='Power Outlet 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)
|
rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1')
|
||||||
front_port = FrontPort.objects.create(
|
front_port = FrontPort.objects.create(device=devices[0], name='Front Port 1')
|
||||||
device=devices[0],
|
PortMapping.objects.create(device=devices[0], front_port=front_port, rear_port=rear_port)
|
||||||
name='Front Port 1',
|
|
||||||
rear_port=rear_port,
|
|
||||||
rear_port_position=1
|
|
||||||
)
|
|
||||||
|
|
||||||
power_panel = PowerPanel.objects.create(name='Power Panel 1', site=sites[0])
|
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)
|
power_feed = PowerFeed.objects.create(name='Power Feed 1', power_panel=power_panel)
|
||||||
|
|||||||
@ -193,7 +193,8 @@ class FrontPortTestCase(TestCase):
|
|||||||
'name': 'FrontPort[1-4]',
|
'name': 'FrontPort[1-4]',
|
||||||
'label': 'Port[1-4]',
|
'label': 'Port[1-4]',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'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)
|
form = FrontPortCreateForm(front_port_data)
|
||||||
|
|
||||||
@ -208,7 +209,8 @@ class FrontPortTestCase(TestCase):
|
|||||||
'name': 'FrontPort[1-4]',
|
'name': 'FrontPort[1-4]',
|
||||||
'label': 'Port[1-2]',
|
'label': 'Port[1-2]',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'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)
|
form = FrontPortCreateForm(bad_front_port_data)
|
||||||
|
|
||||||
|
|||||||
@ -444,13 +444,19 @@ class DeviceTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
rearport.save()
|
rearport.save()
|
||||||
|
|
||||||
FrontPortTemplate(
|
frontport = FrontPortTemplate(
|
||||||
device_type=device_type,
|
device_type=device_type,
|
||||||
name='Front Port 1',
|
name='Front Port 1',
|
||||||
type=PortTypeChoices.TYPE_8P8C,
|
type=PortTypeChoices.TYPE_8P8C,
|
||||||
|
)
|
||||||
|
frontport.save()
|
||||||
|
|
||||||
|
PortTemplateMapping.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
front_port=frontport,
|
||||||
rear_port=rearport,
|
rear_port=rearport,
|
||||||
rear_port_position=2
|
rear_port_position=2,
|
||||||
).save()
|
)
|
||||||
|
|
||||||
ModuleBayTemplate(
|
ModuleBayTemplate(
|
||||||
device_type=device_type,
|
device_type=device_type,
|
||||||
@ -528,11 +534,12 @@ class DeviceTestCase(TestCase):
|
|||||||
device=device,
|
device=device,
|
||||||
name='Front Port 1',
|
name='Front Port 1',
|
||||||
type=PortTypeChoices.TYPE_8P8C,
|
type=PortTypeChoices.TYPE_8P8C,
|
||||||
rear_port=rearport,
|
positions=1
|
||||||
rear_port_position=2
|
|
||||||
)
|
)
|
||||||
self.assertEqual(frontport.cf['cf1'], 'foo')
|
self.assertEqual(frontport.cf['cf1'], 'foo')
|
||||||
|
|
||||||
|
self.assertTrue(PortMapping.objects.filter(front_port=frontport, rear_port=rearport).exists())
|
||||||
|
|
||||||
modulebay = ModuleBay.objects.get(
|
modulebay = ModuleBay.objects.get(
|
||||||
device=device,
|
device=device,
|
||||||
name='Module Bay 1'
|
name='Module Bay 1'
|
||||||
@ -835,12 +842,18 @@ class CableTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
RearPort.objects.bulk_create(rear_ports)
|
RearPort.objects.bulk_create(rear_ports)
|
||||||
front_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='FP1', type='8p8c'),
|
||||||
FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1),
|
FrontPort(device=patch_panel, name='FP2', type='8p8c'),
|
||||||
FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1),
|
FrontPort(device=patch_panel, name='FP3', type='8p8c'),
|
||||||
FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1),
|
FrontPort(device=patch_panel, name='FP4', type='8p8c'),
|
||||||
)
|
)
|
||||||
FrontPort.objects.bulk_create(front_ports)
|
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 = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
|
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
|
||||||
|
|||||||
@ -741,17 +741,16 @@ class DeviceTypeTestCase(
|
|||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
front_ports = (
|
front_ports = (
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(device_type=devicetype, name='Front Port 1'),
|
||||||
device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
|
FrontPortTemplate(device_type=devicetype, name='Front Port 2'),
|
||||||
),
|
FrontPortTemplate(device_type=devicetype, name='Front Port 3'),
|
||||||
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.objects.bulk_create(front_ports)
|
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})
|
url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
|
||||||
self.assertHttpStatus(self.client.get(url), 200)
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
@ -866,12 +865,16 @@ rear-ports:
|
|||||||
front-ports:
|
front-ports:
|
||||||
- name: Front Port 1
|
- name: Front Port 1
|
||||||
type: 8p8c
|
type: 8p8c
|
||||||
rear_port: Rear Port 1
|
|
||||||
- name: Front Port 2
|
- name: Front Port 2
|
||||||
type: 8p8c
|
type: 8p8c
|
||||||
rear_port: Rear Port 2
|
|
||||||
- name: Front Port 3
|
- name: Front Port 3
|
||||||
type: 8p8c
|
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
|
rear_port: Rear Port 3
|
||||||
module-bays:
|
module-bays:
|
||||||
- name: Module Bay 1
|
- name: Module Bay 1
|
||||||
@ -971,8 +974,12 @@ inventory-items:
|
|||||||
self.assertEqual(device_type.frontporttemplates.count(), 3)
|
self.assertEqual(device_type.frontporttemplates.count(), 3)
|
||||||
fp1 = FrontPortTemplate.objects.first()
|
fp1 = FrontPortTemplate.objects.first()
|
||||||
self.assertEqual(fp1.name, 'Front Port 1')
|
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)
|
self.assertEqual(device_type.modulebaytemplates.count(), 3)
|
||||||
mb1 = ModuleBayTemplate.objects.first()
|
mb1 = ModuleBayTemplate.objects.first()
|
||||||
@ -1316,17 +1323,16 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rear_ports)
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
front_ports = (
|
front_ports = (
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(module_type=moduletype, name='Front Port 1'),
|
||||||
module_type=moduletype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1
|
FrontPortTemplate(module_type=moduletype, name='Front Port 2'),
|
||||||
),
|
FrontPortTemplate(module_type=moduletype, name='Front Port 3'),
|
||||||
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.objects.bulk_create(front_ports)
|
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})
|
url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk})
|
||||||
self.assertHttpStatus(self.client.get(url), 200)
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
@ -1394,12 +1400,16 @@ rear-ports:
|
|||||||
front-ports:
|
front-ports:
|
||||||
- name: Front Port 1
|
- name: Front Port 1
|
||||||
type: 8p8c
|
type: 8p8c
|
||||||
rear_port: Rear Port 1
|
|
||||||
- name: Front Port 2
|
- name: Front Port 2
|
||||||
type: 8p8c
|
type: 8p8c
|
||||||
rear_port: Rear Port 2
|
|
||||||
- name: Front Port 3
|
- name: Front Port 3
|
||||||
type: 8p8c
|
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
|
rear_port: Rear Port 3
|
||||||
module-bays:
|
module-bays:
|
||||||
- name: Module Bay 1
|
- name: Module Bay 1
|
||||||
@ -1477,8 +1487,12 @@ module-bays:
|
|||||||
self.assertEqual(module_type.frontporttemplates.count(), 3)
|
self.assertEqual(module_type.frontporttemplates.count(), 3)
|
||||||
fp1 = FrontPortTemplate.objects.first()
|
fp1 = FrontPortTemplate.objects.first()
|
||||||
self.assertEqual(fp1.name, 'Front Port 1')
|
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)
|
self.assertEqual(module_type.modulebaytemplates.count(), 3)
|
||||||
mb1 = ModuleBayTemplate.objects.first()
|
mb1 = ModuleBayTemplate.objects.first()
|
||||||
@ -1770,7 +1784,7 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
|
|||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-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 1'),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
|
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 5'),
|
||||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'),
|
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'),
|
||||||
)
|
)
|
||||||
RearPortTemplate.objects.bulk_create(rearports)
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
|
front_ports = (
|
||||||
FrontPortTemplate.objects.bulk_create(
|
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1'),
|
||||||
(
|
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2'),
|
||||||
FrontPortTemplate(
|
FrontPortTemplate(device_type=devicetype, name='Front Port Template 3'),
|
||||||
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
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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 = {
|
cls.form_data = {
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Front Port X',
|
'name': 'Front Port X',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rearports[3].pk,
|
'positions': 1,
|
||||||
'rear_port_position': 1,
|
'rear_ports': [f'{rear_ports[3].pk}:1'],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.bulk_create_data = {
|
cls.bulk_create_data = {
|
||||||
'device_type': devicetype.pk,
|
'device_type': devicetype.pk,
|
||||||
'name': 'Front Port [4-6]',
|
'name': 'Front Port [4-6]',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'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 = {
|
cls.bulk_edit_data = {
|
||||||
@ -2276,11 +2288,16 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
RearPort.objects.bulk_create(rear_ports)
|
RearPort.objects.bulk_create(rear_ports)
|
||||||
front_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 Template 1'),
|
||||||
FrontPort(device=device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
|
FrontPort(device=device, name='Front Port Template 2'),
|
||||||
FrontPort(device=device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
|
FrontPort(device=device, name='Front Port Template 3'),
|
||||||
)
|
)
|
||||||
FrontPort.objects.bulk_create(front_ports)
|
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})
|
url = reverse('dcim:device_frontports', kwargs={'pk': device.pk})
|
||||||
self.assertHttpStatus(self.client.get(url), 200)
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
@ -3065,7 +3082,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
device = create_test_device('Device 1')
|
device = create_test_device('Device 1')
|
||||||
|
|
||||||
rearports = (
|
rear_ports = (
|
||||||
RearPort(device=device, name='Rear Port 1'),
|
RearPort(device=device, name='Rear Port 1'),
|
||||||
RearPort(device=device, name='Rear Port 2'),
|
RearPort(device=device, name='Rear Port 2'),
|
||||||
RearPort(device=device, name='Rear Port 3'),
|
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 5'),
|
||||||
RearPort(device=device, name='Rear Port 6'),
|
RearPort(device=device, name='Rear Port 6'),
|
||||||
)
|
)
|
||||||
RearPort.objects.bulk_create(rearports)
|
RearPort.objects.bulk_create(rear_ports)
|
||||||
|
|
||||||
front_ports = (
|
front_ports = (
|
||||||
FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]),
|
FrontPort(device=device, name='Front Port 1'),
|
||||||
FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]),
|
FrontPort(device=device, name='Front Port 2'),
|
||||||
FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
|
FrontPort(device=device, name='Front Port 3'),
|
||||||
)
|
)
|
||||||
FrontPort.objects.bulk_create(front_ports)
|
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')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
@ -3088,8 +3110,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Front Port X',
|
'name': 'Front Port X',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'type': PortTypeChoices.TYPE_8P8C,
|
||||||
'rear_port': rearports[3].pk,
|
'positions': 1,
|
||||||
'rear_port_position': 1,
|
'rear_ports': [f'{rear_ports[3].pk}:1'],
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
@ -3098,7 +3120,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'device': device.pk,
|
'device': device.pk,
|
||||||
'name': 'Front Port [4-6]',
|
'name': 'Front Port [4-6]',
|
||||||
'type': PortTypeChoices.TYPE_8P8C,
|
'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',
|
'description': 'New description',
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
@ -3109,10 +3132,10 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"device,name,type,rear_port,rear_port_position",
|
"device,name,type,positions",
|
||||||
"Device 1,Front Port 4,8p8c,Rear Port 4,1",
|
"Device 1,Front Port 4,8p8c,1",
|
||||||
"Device 1,Front Port 5,8p8c,Rear Port 5,1",
|
"Device 1,Front Port 5,8p8c,1",
|
||||||
"Device 1,Front Port 6,8p8c,Rear Port 6,1",
|
"Device 1,Front Port 6,8p8c,1",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
|||||||
@ -83,3 +83,36 @@ def update_interface_bridges(device, interface_templates, module=None):
|
|||||||
)
|
)
|
||||||
interface.full_clean()
|
interface.full_clean()
|
||||||
interface.save()
|
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 . import filtersets, forms, tables
|
||||||
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from .models.device_components import PortMapping
|
||||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
from .object_actions import BulkAddComponents, BulkDisconnect
|
||||||
|
|
||||||
CABLE_TERMINATION_TYPES = {
|
CABLE_TERMINATION_TYPES = {
|
||||||
@ -1515,6 +1516,7 @@ class DeviceTypeImportView(generic.BulkImportView):
|
|||||||
'interfaces': forms.InterfaceTemplateImportForm,
|
'interfaces': forms.InterfaceTemplateImportForm,
|
||||||
'rear-ports': forms.RearPortTemplateImportForm,
|
'rear-ports': forms.RearPortTemplateImportForm,
|
||||||
'front-ports': forms.FrontPortTemplateImportForm,
|
'front-ports': forms.FrontPortTemplateImportForm,
|
||||||
|
'port-mappings': forms.PortTemplateMappingImportForm,
|
||||||
'module-bays': forms.ModuleBayTemplateImportForm,
|
'module-bays': forms.ModuleBayTemplateImportForm,
|
||||||
'device-bays': forms.DeviceBayTemplateImportForm,
|
'device-bays': forms.DeviceBayTemplateImportForm,
|
||||||
'inventory-items': forms.InventoryItemTemplateImportForm,
|
'inventory-items': forms.InventoryItemTemplateImportForm,
|
||||||
@ -1819,6 +1821,7 @@ class ModuleTypeImportView(generic.BulkImportView):
|
|||||||
'interfaces': forms.InterfaceTemplateImportForm,
|
'interfaces': forms.InterfaceTemplateImportForm,
|
||||||
'rear-ports': forms.RearPortTemplateImportForm,
|
'rear-ports': forms.RearPortTemplateImportForm,
|
||||||
'front-ports': forms.FrontPortTemplateImportForm,
|
'front-ports': forms.FrontPortTemplateImportForm,
|
||||||
|
'port-mappings': forms.PortTemplateMappingImportForm,
|
||||||
'module-bays': forms.ModuleBayTemplateImportForm,
|
'module-bays': forms.ModuleBayTemplateImportForm,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3242,6 +3245,11 @@ class FrontPortListView(generic.ObjectListView):
|
|||||||
class FrontPortView(generic.ObjectView):
|
class FrontPortView(generic.ObjectView):
|
||||||
queryset = FrontPort.objects.all()
|
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)
|
@register_model_view(FrontPort, 'add', detail=False)
|
||||||
class FrontPortCreateView(generic.ComponentCreateView):
|
class FrontPortCreateView(generic.ComponentCreateView):
|
||||||
@ -3313,6 +3321,11 @@ class RearPortListView(generic.ObjectListView):
|
|||||||
class RearPortView(generic.ObjectView):
|
class RearPortView(generic.ObjectView):
|
||||||
queryset = RearPort.objects.all()
|
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)
|
@register_model_view(RearPort, 'add', detail=False)
|
||||||
class RearPortCreateView(generic.ComponentCreateView):
|
class RearPortCreateView(generic.ComponentCreateView):
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import router, transaction
|
from django.db import router, transaction
|
||||||
@ -563,7 +562,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
new_components = []
|
new_components = []
|
||||||
data = deepcopy(request.POST)
|
data = request.POST.copy()
|
||||||
pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
|
pattern_count = len(form.cleaned_data[self.form.replication_fields[0]])
|
||||||
|
|
||||||
for i in range(pattern_count):
|
for i in range(pattern_count):
|
||||||
@ -572,7 +571,8 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||||||
data[field_name] = form.cleaned_data[field_name][i]
|
data[field_name] = form.cleaned_data[field_name][i]
|
||||||
|
|
||||||
if hasattr(form, 'get_iterative_data'):
|
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)
|
component_form = self.model_form(data)
|
||||||
|
|
||||||
|
|||||||
@ -47,12 +47,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Rear Port" %}</th>
|
<th scope="row">{% trans "Positions" %}</th>
|
||||||
<td>{{ object.rear_port|linkify }}</td>
|
<td>{{ object.positions }}</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Rear Port Position" %}</th>
|
|
||||||
<td>{{ object.rear_port_position }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
@ -65,6 +61,18 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-12 col-md-6">
|
<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">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||||
{% if object.mark_connected %}
|
{% if object.mark_connected %}
|
||||||
|
|||||||
@ -61,6 +61,18 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-12 col-md-6">
|
<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">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||||
{% if object.mark_connected %}
|
{% if object.mark_connected %}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def get_related_models(model, ordered=True):
|
|||||||
related_models = [
|
related_models = [
|
||||||
(field.related_model, field.remote_field.name)
|
(field.related_model, field.remote_field.name)
|
||||||
for field in model._meta.related_objects
|
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:
|
if ordered:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user