diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py index 1dca773b2..3b9142f1d 100644 --- a/netbox/dcim/api/serializers_/base.py +++ b/netbox/dcim/api/serializers_/base.py @@ -2,10 +2,12 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers +from dcim.models import FrontPort, FrontPortTemplate, PortMapping, PortTemplateMapping, RearPort, RearPortTemplate from utilities.api import get_serializer_for_model __all__ = ( 'ConnectedEndpointsSerializer', + 'PortSerializer', ) @@ -35,3 +37,53 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer): @extend_schema_field(serializers.BooleanField) def get_connected_endpoints_reachable(self, obj): return obj._path and obj._path.is_complete and obj._path.is_active + + +class PortSerializer(serializers.ModelSerializer): + """ + Base serializer for front & rear port and port templates. + """ + @property + def _mapper(self): + """ + Return the model and ForeignKey field name used to track port mappings for this model. + """ + if self.Meta.model is FrontPort: + return PortMapping, 'front_port' + if self.Meta.model is RearPort: + return PortMapping, 'rear_port' + if self.Meta.model is FrontPortTemplate: + return PortTemplateMapping, 'front_port' + if self.Meta.model is RearPortTemplate: + return PortTemplateMapping, 'rear_port' + raise ValueError(f"Could not determine mapping details for {self.__class__}") + + def create(self, validated_data): + mappings = validated_data.pop('mappings', []) + instance = super().create(validated_data) + + # Create port mappings + mapping_model, fk_name = self._mapper + for attrs in mappings: + mapping_model.objects.create(**{ + fk_name: instance, + **attrs, + }) + + return instance + + def update(self, instance, validated_data): + mappings = validated_data.pop('mappings', None) + instance = super().update(instance, validated_data) + + if mappings is not None: + # Update port mappings + mapping_model, fk_name = self._mapper + mapping_model.objects.filter(**{fk_name: instance}).delete() + for attrs in mappings: + mapping_model.objects.create(**{ + fk_name: instance, + **attrs, + }) + + return instance diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index b26cf9bbb..99940b942 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -5,21 +5,21 @@ from rest_framework import serializers from dcim.choices import * from dcim.constants import * from dcim.models import ( - ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, - RearPort, VirtualDeviceContext, + ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PortMapping, + PowerOutlet, PowerPort, RearPort, VirtualDeviceContext, ) from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer from ipam.api.serializers_.vrfs import VRFSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.gfk_fields import GFKSerializerField -from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from netbox.api.serializers import NetBoxModelSerializer from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from wireless.api.serializers_.nested import NestedWirelessLinkSerializer from wireless.api.serializers_.wirelesslans import WirelessLANSerializer from wireless.choices import * from wireless.models import WirelessLAN -from .base import ConnectedEndpointsSerializer +from .base import ConnectedEndpointsSerializer, PortSerializer from .cables import CabledObjectSerializer from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer from .manufacturers import ManufacturerSerializer @@ -294,7 +294,20 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect return super().validate(data) -class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): +class RearPortMappingSerializer(serializers.ModelSerializer): + position = serializers.IntegerField( + source='rear_port_position' + ) + front_port = serializers.PrimaryKeyRelatedField( + queryset=FrontPort.objects.all(), + ) + + class Meta: + model = PortMapping + fields = ('position', 'front_port', 'front_port_position') + + +class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer): device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, @@ -303,28 +316,36 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): allow_null=True ) type = ChoiceField(choices=PortTypeChoices) + front_ports = RearPortMappingSerializer( + source='mappings', + many=True, + required=False, + ) class Meta: model = RearPort fields = [ 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', - 'custom_fields', 'created', 'last_updated', '_occupied', + 'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') -class FrontPortRearPortSerializer(WritableNestedSerializer): - """ - NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) - """ +class FrontPortMappingSerializer(serializers.ModelSerializer): + position = serializers.IntegerField( + source='front_port_position' + ) + rear_port = serializers.PrimaryKeyRelatedField( + queryset=RearPort.objects.all(), + ) class Meta: - model = RearPort - fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description'] + model = PortMapping + fields = ('position', 'rear_port', 'rear_port_position') -class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): +class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer): device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, @@ -333,14 +354,18 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): allow_null=True ) type = ChoiceField(choices=PortTypeChoices) - rear_port = FrontPortRearPortSerializer() + rear_ports = FrontPortMappingSerializer( + source='mappings', + many=True, + required=False, + ) class Meta: model = FrontPort fields = [ - 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', - 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', + 'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py index b44565d65..9a9b5d470 100644 --- a/netbox/dcim/api/serializers_/devicetype_components.py +++ b/netbox/dcim/api/serializers_/devicetype_components.py @@ -5,12 +5,14 @@ from dcim.choices import * from dcim.constants import * from dcim.models import ( ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, - InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, + InventoryItemTemplate, ModuleBayTemplate, PortTemplateMapping, PowerOutletTemplate, PowerPortTemplate, + RearPortTemplate, ) from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer from wireless.choices import * +from .base import PortSerializer from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer from .manufacturers import ManufacturerSerializer from .nested import NestedInterfaceTemplateSerializer @@ -205,7 +207,20 @@ class InterfaceTemplateSerializer(ComponentTemplateSerializer): brief_fields = ('id', 'url', 'display', 'name', 'description') -class RearPortTemplateSerializer(ComponentTemplateSerializer): +class RearPortTemplateMappingSerializer(serializers.ModelSerializer): + position = serializers.IntegerField( + source='rear_port_position' + ) + front_port = serializers.PrimaryKeyRelatedField( + queryset=FrontPortTemplate.objects.all(), + ) + + class Meta: + model = PortTemplateMapping + fields = ('position', 'front_port', 'front_port_position') + + +class RearPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer): device_type = DeviceTypeSerializer( required=False, nested=True, @@ -219,17 +234,35 @@ class RearPortTemplateSerializer(ComponentTemplateSerializer): default=None ) type = ChoiceField(choices=PortTypeChoices) + front_ports = RearPortTemplateMappingSerializer( + source='mappings', + many=True, + required=False, + ) class Meta: model = RearPortTemplate fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', - 'positions', 'description', 'created', 'last_updated', + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', + 'front_ports', 'description', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') -class FrontPortTemplateSerializer(ComponentTemplateSerializer): +class FrontPortTemplateMappingSerializer(serializers.ModelSerializer): + position = serializers.IntegerField( + source='front_port_position' + ) + rear_port = serializers.PrimaryKeyRelatedField( + queryset=RearPortTemplate.objects.all(), + ) + + class Meta: + model = PortTemplateMapping + fields = ('position', 'rear_port', 'rear_port_position') + + +class FrontPortTemplateSerializer(ComponentTemplateSerializer, PortSerializer): device_type = DeviceTypeSerializer( nested=True, required=False, @@ -243,13 +276,17 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer): default=None ) type = ChoiceField(choices=PortTypeChoices) - rear_port = RearPortTemplateSerializer(nested=True) + rear_ports = FrontPortTemplateMappingSerializer( + source='mappings', + many=True, + required=False, + ) class Meta: model = FrontPortTemplate fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', - 'rear_port', 'rear_port_position', 'description', 'created', 'last_updated', + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', + 'rear_ports', 'description', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 5e4311c13..16926081f 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -32,8 +32,8 @@ CABLE_POSITION_MAX = 1024 # RearPorts # -REARPORT_POSITIONS_MIN = 1 -REARPORT_POSITIONS_MAX = 1024 +PORT_POSITION_MIN = 1 +PORT_POSITION_MAX = 1024 # diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index a3f8ff383..0cd75f954 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -904,12 +904,15 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo null_value=None ) rear_port_id = django_filters.ModelMultipleChoiceFilter( - queryset=RearPort.objects.all() + field_name='mappings__rear_port', + queryset=RearPort.objects.all(), + to_field_name='rear_port', + label=_('Rear port (ID)'), ) class Meta: model = FrontPortTemplate - fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description') + fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description') @register_filterset @@ -918,6 +921,12 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom choices=PortTypeChoices, null_value=None ) + front_port_id = django_filters.ModelMultipleChoiceFilter( + field_name='mappings__front_port', + queryset=FrontPort.objects.all(), + to_field_name='front_port', + label=_('Front port (ID)'), + ) class Meta: model = RearPortTemplate @@ -2137,13 +2146,16 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet) null_value=None ) rear_port_id = django_filters.ModelMultipleChoiceFilter( - queryset=RearPort.objects.all() + field_name='mappings__rear_port', + queryset=RearPort.objects.all(), + to_field_name='rear_port', + label=_('Rear port (ID)'), ) class Meta: model = FrontPort fields = ( - 'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end', + 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', 'cable_position', ) @@ -2154,6 +2166,12 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet): choices=PortTypeChoices, null_value=None ) + front_port_id = django_filters.ModelMultipleChoiceFilter( + field_name='mappings__front_port', + queryset=FrontPort.objects.all(), + to_field_name='front_port', + label=_('Front port (ID)'), + ) class Meta: model = RearPort diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index ba0b44b0d..68258aa8b 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -1075,12 +1075,6 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): queryset=Device.objects.all(), to_field_name='name' ) - rear_port = CSVModelChoiceField( - label=_('Rear port'), - queryset=RearPort.objects.all(), - to_field_name='name', - help_text=_('Corresponding rear port') - ) type = CSVChoiceField( label=_('Type'), choices=PortTypeChoices, @@ -1090,32 +1084,9 @@ class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): class Meta: model = FrontPort fields = ( - 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position', - 'description', 'owner', 'tags' + 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'owner', 'tags' ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit RearPort choices to those belonging to this device (or VC master) - if self.is_bound and 'device' in self.data: - try: - device = self.fields['device'].to_python(self.data['device']) - except forms.ValidationError: - device = None - else: - try: - device = self.instance.device - except Device.DoesNotExist: - device = None - - if device: - self.fields['rear_port'].queryset = RearPort.objects.filter( - device__in=[device, device.get_vc_master()] - ) - else: - self.fields['rear_port'].queryset = RearPort.objects.none() - class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( diff --git a/netbox/dcim/forms/mixins.py b/netbox/dcim/forms/mixins.py index 96eb8a56b..b2fc46bc3 100644 --- a/netbox/dcim/forms/mixins.py +++ b/netbox/dcim/forms/mixins.py @@ -1,10 +1,12 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db import connection +from django.db.models.signals import post_save from django.utils.translation import gettext_lazy as _ from dcim.constants import LOCATION_SCOPE_TYPES -from dcim.models import Site +from dcim.models import PortMapping, PortTemplateMapping, Site from utilities.forms import get_field_value from utilities.forms.fields import ( ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField, @@ -13,6 +15,7 @@ from utilities.templatetags.builtins.filters import bettertitle from utilities.forms.widgets import HTMXSelect __all__ = ( + 'FrontPortFormMixin', 'ScopedBulkEditForm', 'ScopedForm', 'ScopedImportForm', @@ -128,3 +131,75 @@ class ScopedImportForm(forms.Form): "Please select a {scope_type}." ).format(scope_type=scope_type.model_class()._meta.model_name) }) + + +class FrontPortFormMixin(forms.Form): + rear_ports = forms.MultipleChoiceField( + choices=[], + label=_('Rear ports'), + widget=forms.SelectMultiple(attrs={'size': 8}) + ) + + port_mapping_model = PortMapping + parent_field = 'device' + + def clean(self): + super().clean() + + # Check that the total number of FrontPorts and positions matches the selected number of RearPort:position + # mappings. Note that `name` will be a list under FrontPortCreateForm, in which cases we multiply the number of + # FrontPorts being creation by the number of positions. + positions = self.cleaned_data['positions'] + frontport_count = len(self.cleaned_data['name']) if type(self.cleaned_data['name']) is list else 1 + rearport_count = len(self.cleaned_data['rear_ports']) + if frontport_count * positions != rearport_count: + raise forms.ValidationError({ + 'rear_ports': _( + "The total number of front port positions ({frontport_count}) must match the selected number of " + "rear port positions ({rearport_count})." + ).format( + frontport_count=frontport_count, + rearport_count=rearport_count + ) + }) + + def _save_m2m(self): + super()._save_m2m() + + # TODO: Can this be made more efficient? + # Delete existing rear port mappings + self.port_mapping_model.objects.filter(front_port_id=self.instance.pk).delete() + + # Create new rear port mappings + mappings = [] + if self.port_mapping_model is PortTemplateMapping: + params = { + 'device_type_id': self.instance.device_type_id, + 'module_type_id': self.instance.module_type_id, + } + else: + params = { + 'device_id': self.instance.device_id, + } + for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1): + rear_port_id, rear_port_position = rp_position.split(':') + mappings.append( + self.port_mapping_model(**{ + **params, + 'front_port_id': self.instance.pk, + 'front_port_position': i, + 'rear_port_id': rear_port_id, + 'rear_port_position': rear_port_position, + }) + ) + self.port_mapping_model.objects.bulk_create(mappings) + # Send post_save signals + for mapping in mappings: + post_save.send( + sender=PortMapping, + instance=mapping, + created=True, + raw=False, + using=connection, + update_fields=None + ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 66e54b887..709ecdfe0 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -6,6 +6,7 @@ from timezone_field import TimeZoneFormField from dcim.choices import * from dcim.constants import * +from dcim.forms.mixins import FrontPortFormMixin from dcim.models import * from extras.models import ConfigTemplate from ipam.choices import VLANQinQRoleChoices @@ -1112,34 +1113,66 @@ class InterfaceTemplateForm(ModularComponentTemplateForm): ] -class FrontPortTemplateForm(ModularComponentTemplateForm): - rear_port = DynamicModelChoiceField( - label=_('Rear port'), - queryset=RearPortTemplate.objects.all(), - required=False, - query_params={ - 'device_type_id': '$device_type', - 'module_type_id': '$module_type', - } - ) - +class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm): fieldsets = ( FieldSet( TabbedGroups( FieldSet('device_type', name=_('Device Type')), FieldSet('module_type', name=_('Module Type')), ), - 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', + 'name', 'label', 'type', 'positions', 'rear_ports', 'description', ), ) + # Override FrontPortFormMixin attrs + port_mapping_model = PortTemplateMapping + parent_field = 'device_type' + class Meta: model = FrontPortTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', - 'description', + 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description', ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if device_type_id := self.data.get('device_type') or self.initial.get('device_type'): + device_type = DeviceType.objects.get(pk=device_type_id) + else: + return + + # Populate rear port choices + self.fields['rear_ports'].choices = self._get_rear_port_choices(device_type, self.instance) + + # Set initial rear port mappings + if self.instance.pk: + self.initial['rear_ports'] = [ + f'{mapping.rear_port_id}:{mapping.rear_port_position}' + for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk) + ] + + def _get_rear_port_choices(self, device_type, front_port): + """ + Return a list of choices representing each available rear port & position pair on the device type, excluding + those assigned to the specified instance. + """ + occupied_rear_port_positions = [ + f'{mapping.rear_port_id}:{mapping.rear_port_position}' + for mapping in device_type.port_mappings.exclude(front_port=front_port.pk) + ] + + choices = [] + for rear_port in RearPortTemplate.objects.filter(device_type=device_type): + for i in range(1, rear_port.positions + 1): + pair_id = f'{rear_port.pk}:{i}' + if pair_id not in occupied_rear_port_positions: + pair_label = f'{rear_port.name}:{i}' + choices.append( + (pair_id, pair_label) + ) + return choices + class RearPortTemplateForm(ModularComponentTemplateForm): fieldsets = ( @@ -1578,17 +1611,10 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): } -class FrontPortForm(ModularDeviceComponentForm): - rear_port = DynamicModelChoiceField( - queryset=RearPort.objects.all(), - query_params={ - 'device_id': '$device', - } - ) - +class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm): fieldsets = ( FieldSet( - 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', + 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected', 'description', 'tags', ), ) @@ -1596,10 +1622,49 @@ class FrontPortForm(ModularDeviceComponentForm): class Meta: model = FrontPort fields = [ - 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', - 'description', 'owner', 'tags', + 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', + 'tags', ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if device_id := self.data.get('device') or self.initial.get('device'): + device = Device.objects.get(pk=device_id) + else: + return + + # Populate rear port choices + self.fields['rear_ports'].choices = self._get_rear_port_choices(device, self.instance) + + # Set initial rear port mappings + if self.instance.pk: + self.initial['rear_ports'] = [ + f'{mapping.rear_port_id}:{mapping.rear_port_position}' + for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk) + ] + + def _get_rear_port_choices(self, device, front_port): + """ + Return a list of choices representing each available rear port & position pair on the device, excluding those + assigned to the specified instance. + """ + occupied_rear_port_positions = [ + f'{mapping.rear_port_id}:{mapping.rear_port_position}' + for mapping in device.port_mappings.exclude(front_port=front_port.pk) + ] + + choices = [] + for rear_port in RearPort.objects.filter(device=device): + for i in range(1, rear_port.positions + 1): + pair_id = f'{rear_port.pk}:{i}' + if pair_id not in occupied_rear_port_positions: + pair_label = f'{rear_port.name}:{i}' + choices.append( + (pair_id, pair_label) + ) + return choices + class RearPortForm(ModularDeviceComponentForm): fieldsets = ( diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index d9afabddf..8e0818326 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -109,85 +109,30 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm): - rear_port = forms.MultipleChoiceField( - choices=[], - label=_('Rear ports'), - help_text=_('Select one rear port assignment for each front port being created.'), - widget=forms.SelectMultiple(attrs={'size': 6}) - ) - # Override fieldsets from FrontPortTemplateForm to omit rear_port_position + # Override fieldsets from FrontPortTemplateForm fieldsets = ( FieldSet( TabbedGroups( FieldSet('device_type', name=_('Device Type')), FieldSet('module_type', name=_('Module Type')), ), - 'name', 'label', 'type', 'color', 'rear_port', 'description', + 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description', ), ) - class Meta(model_forms.FrontPortTemplateForm.Meta): - exclude = ('name', 'label', 'rear_port', 'rear_port_position') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # TODO: This needs better validation - if 'device_type' in self.initial or self.data.get('device_type'): - parent = DeviceType.objects.get( - pk=self.initial.get('device_type') or self.data.get('device_type') - ) - elif 'module_type' in self.initial or self.data.get('module_type'): - parent = ModuleType.objects.get( - pk=self.initial.get('module_type') or self.data.get('module_type') - ) - else: - return - - # Determine which rear port positions are occupied. These will be excluded from the list of available mappings. - occupied_port_positions = [ - (front_port.rear_port_id, front_port.rear_port_position) - for front_port in parent.frontporttemplates.all() - ] - - # Populate rear port choices - choices = [] - rear_ports = parent.rearporttemplates.all() - for rear_port in rear_ports: - for i in range(1, rear_port.positions + 1): - if (rear_port.pk, i) not in occupied_port_positions: - choices.append( - ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) - ) - self.fields['rear_port'].choices = choices - - def clean(self): - super().clean() - - # Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate - # positions - frontport_count = len(self.cleaned_data['name']) - rearport_count = len(self.cleaned_data['rear_port']) - if frontport_count != rearport_count: - raise forms.ValidationError({ - 'rear_port': _( - "The number of front port templates to be created ({frontport_count}) must match the selected " - "number of rear port positions ({rearport_count})." - ).format( - frontport_count=frontport_count, - rearport_count=rearport_count - ) - }) + class Meta: + model = FrontPortTemplate + fields = ( + 'device_type', 'module_type', 'type', 'color', 'positions', 'description', + ) def get_iterative_data(self, iteration): - - # Assign rear port and position from selected set - rear_port, position = self.cleaned_data['rear_port'][iteration].split(':') + positions = self.cleaned_data['positions'] + offset = positions * iteration return { - 'rear_port': int(rear_port), - 'rear_port_position': int(position), + 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions] } @@ -269,74 +214,26 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm): } ) ) - rear_port = forms.MultipleChoiceField( - choices=[], - label=_('Rear ports'), - help_text=_('Select one rear port assignment for each front port being created.'), - widget=forms.SelectMultiple(attrs={'size': 6}) - ) # Override fieldsets from FrontPortForm to omit rear_port_position fieldsets = ( FieldSet( - 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected', + 'description', 'tags', ), ) - class Meta(model_forms.FrontPortForm.Meta): - exclude = ('name', 'label', 'rear_port', 'rear_port_position') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if device_id := self.data.get('device') or self.initial.get('device'): - device = Device.objects.get(pk=device_id) - else: - return - - # Determine which rear port positions are occupied. These will be excluded from the list of available - # mappings. - occupied_port_positions = [ - (front_port.rear_port_id, front_port.rear_port_position) - for front_port in device.frontports.all() + class Meta: + model = FrontPort + fields = [ + 'device', 'module', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', 'tags', ] - # Populate rear port choices - choices = [] - rear_ports = RearPort.objects.filter(device=device) - for rear_port in rear_ports: - for i in range(1, rear_port.positions + 1): - if (rear_port.pk, i) not in occupied_port_positions: - choices.append( - ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) - ) - self.fields['rear_port'].choices = choices - - def clean(self): - super().clean() - - # Check that the number of FrontPorts to be created matches the selected number of RearPort positions - frontport_count = len(self.cleaned_data['name']) - rearport_count = len(self.cleaned_data['rear_port']) - if frontport_count != rearport_count: - raise forms.ValidationError({ - 'rear_port': _( - "The number of front ports to be created ({frontport_count}) must match the selected number of " - "rear port positions ({rearport_count})." - ).format( - frontport_count=frontport_count, - rearport_count=rearport_count - ) - }) - def get_iterative_data(self, iteration): - - # Assign rear port and position from selected set - rear_port, position = self.cleaned_data['rear_port'][iteration].split(':') - + positions = self.cleaned_data['positions'] + offset = positions * iteration return { - 'rear_port': int(rear_port), - 'rear_port_position': int(position), + 'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions] } diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index 3f2cc3ef6..3b6a6e648 100644 --- a/netbox/dcim/forms/object_import.py +++ b/netbox/dcim/forms/object_import.py @@ -13,6 +13,7 @@ __all__ = ( 'InterfaceTemplateImportForm', 'InventoryItemTemplateImportForm', 'ModuleBayTemplateImportForm', + 'PortTemplateMappingImportForm', 'PowerOutletTemplateImportForm', 'PowerPortTemplateImportForm', 'RearPortTemplateImportForm', @@ -113,31 +114,11 @@ class FrontPortTemplateImportForm(forms.ModelForm): label=_('Type'), choices=PortTypeChoices.CHOICES ) - rear_port = forms.ModelChoiceField( - label=_('Rear port'), - queryset=RearPortTemplate.objects.all(), - to_field_name='name' - ) - - def clean_device_type(self): - if device_type := self.cleaned_data['device_type']: - rear_port = self.fields['rear_port'] - rear_port.queryset = rear_port.queryset.filter(device_type=device_type) - - return device_type - - def clean_module_type(self): - if module_type := self.cleaned_data['module_type']: - rear_port = self.fields['rear_port'] - rear_port.queryset = rear_port.queryset.filter(module_type=module_type) - - return module_type class Meta: model = FrontPortTemplate fields = [ - 'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', - 'description', + 'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description', ] @@ -154,6 +135,25 @@ class RearPortTemplateImportForm(forms.ModelForm): ] +class PortTemplateMappingImportForm(forms.ModelForm): + front_port = forms.ModelChoiceField( + label=_('Front port'), + queryset=FrontPortTemplate.objects.all(), + to_field_name='name', + ) + rear_port = forms.ModelChoiceField( + label=_('Rear port'), + queryset=RearPortTemplate.objects.all(), + to_field_name='name', + ) + + class Meta: + model = PortTemplateMapping + fields = [ + 'front_port', 'front_port_position', 'rear_port', 'rear_port_position', + ] + + class ModuleBayTemplateImportForm(forms.ModelForm): class Meta: diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 1c99beedb..11aff8201 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -6,7 +6,7 @@ import strawberry_django from strawberry.scalars import ID from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup -from core.graphql.filter_mixins import ChangeLogFilterMixin +from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin from dcim import models from dcim.constants import * from dcim.graphql.enums import InterfaceKindEnum @@ -75,6 +75,8 @@ __all__ = ( 'ModuleTypeFilter', 'ModuleTypeProfileFilter', 'PlatformFilter', + 'PortMappingFilter', + 'PortTemplateMappingFilter', 'PowerFeedFilter', 'PowerOutletFilter', 'PowerOutletTemplateFilter', @@ -409,13 +411,6 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = ( strawberry_django.filter_field() ) - rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( - strawberry_django.filter_field() - ) - rear_port_id: ID | None = strawberry_django.filter_field() - rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( - strawberry_django.filter_field() - ) @strawberry_django.filter_type(models.FrontPortTemplate, lookups=True) @@ -426,13 +421,37 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin): color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = ( strawberry_django.filter_field() ) + + +@strawberry_django.filter_type(models.PortMapping, lookups=True) +class PortMappingFilter(BaseObjectTypeFilterMixin): + device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + front_port: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + front_port_position: FilterLookup[int] | None = strawberry_django.filter_field() + rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field() + + +@strawberry_django.filter_type(models.PortTemplateMapping, lookups=True) +class PortTemplateMappingFilter(BaseObjectTypeFilterMixin): + device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + front_port: Annotated['FrontPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() ) - rear_port_id: ID | None = strawberry_django.filter_field() - rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( - strawberry_django.filter_field() - ) + front_port_position: FilterLookup[int] | None = strawberry_django.filter_field() + rear_port_position: FilterLookup[int] | None = strawberry_django.filter_field() @strawberry_django.filter_type(models.MACAddress, lookups=True) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 13408dc90..1132a0ca9 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -385,7 +385,8 @@ class DeviceTypeType(PrimaryObjectType): ) class FrontPortType(ModularComponentType, CabledObjectMixin): color: str - rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')] + + mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]] @strawberry_django.type( @@ -396,7 +397,8 @@ class FrontPortType(ModularComponentType, CabledObjectMixin): ) class FrontPortTemplateType(ModularComponentTemplateType): color: str - rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')] + + mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]] @strawberry_django.type( @@ -636,6 +638,28 @@ class PlatformType(NestedGroupObjectType): devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] +@strawberry_django.type( + models.PortMapping, + fields='__all__', + filters=PortMappingFilter, + pagination=True +) +class PortMappingType(ModularComponentTemplateType): + front_port: Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')] + rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')] + + +@strawberry_django.type( + models.PortTemplateMapping, + fields='__all__', + filters=PortTemplateMappingFilter, + pagination=True +) +class PortMappingTemplateType(ModularComponentTemplateType): + front_port: Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')] + rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')] + + @strawberry_django.type( models.PowerFeed, exclude=['_path'], @@ -768,7 +792,7 @@ class RackRoleType(OrganizationalObjectType): class RearPortType(ModularComponentType, CabledObjectMixin): color: str - frontports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]] + mappings: List[Annotated["PortMappingType", strawberry.lazy('dcim.graphql.types')]] @strawberry_django.type( @@ -780,7 +804,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin): class RearPortTemplateType(ModularComponentTemplateType): color: str - frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] + mappings: List[Annotated["PortMappingTemplateType", strawberry.lazy('dcim.graphql.types')]] @strawberry_django.type( diff --git a/netbox/dcim/migrations/0222_port_mappings.py b/netbox/dcim/migrations/0222_port_mappings.py new file mode 100644 index 000000000..a163ae18d --- /dev/null +++ b/netbox/dcim/migrations/0222_port_mappings.py @@ -0,0 +1,219 @@ +import django.core.validators +import django.db.models.deletion +from django.db import migrations +from django.db import models +from itertools import islice + + +def chunked(iterable, size): + """ + Yield successive chunks of a given size from an iterator. + """ + iterator = iter(iterable) + while chunk := list(islice(iterator, size)): + yield chunk + + +def populate_port_template_mappings(apps, schema_editor): + FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate') + PortTemplateMapping = apps.get_model('dcim', 'PortTemplateMapping') + + front_ports = FrontPortTemplate.objects.iterator(chunk_size=1000) + + def generate_copies(): + for front_port in front_ports: + yield PortTemplateMapping( + device_type_id=front_port.device_type_id, + module_type_id=front_port.module_type_id, + front_port_id=front_port.pk, + front_port_position=1, + rear_port_id=front_port.rear_port_id, + rear_port_position=front_port.rear_port_position, + ) + + # Bulk insert in streaming batches + for chunk in chunked(generate_copies(), 1000): + PortTemplateMapping.objects.bulk_create(chunk, batch_size=1000) + + +def populate_port_mappings(apps, schema_editor): + FrontPort = apps.get_model('dcim', 'FrontPort') + PortMapping = apps.get_model('dcim', 'PortMapping') + + front_ports = FrontPort.objects.iterator(chunk_size=1000) + + def generate_copies(): + for front_port in front_ports: + yield PortMapping( + device_id=front_port.device_id, + front_port_id=front_port.pk, + front_port_position=1, + rear_port_id=front_port.rear_port_id, + rear_port_position=front_port.rear_port_position, + ) + + # Bulk insert in streaming batches + for chunk in chunked(generate_copies(), 1000): + PortMapping.objects.bulk_create(chunk, batch_size=1000) + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0221_cable_position'), + ] + + operations = [ + # Create PortTemplateMapping model (for DeviceTypes) + migrations.CreateModel( + name='PortTemplateMapping', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ( + 'front_port_position', + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024) + ] + ) + ), + ( + 'rear_port_position', + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024) + ] + ) + ), + ( + 'device_type', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.devicetype', + related_name='port_mappings', + blank=True, + null=True + ) + ), + ( + 'module_type', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.moduletype', + related_name='port_mappings', + blank=True, + null=True + ) + ), + ( + 'front_port', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.frontporttemplate', + related_name='mappings' + ) + ), + ( + 'rear_port', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.rearporttemplate', + related_name='mappings' + ) + ), + ], + ), + migrations.AddConstraint( + model_name='porttemplatemapping', + constraint=models.UniqueConstraint( + fields=('front_port', 'front_port_position'), + name='dcim_porttemplatemapping_unique_front_port_position' + ), + ), + migrations.AddConstraint( + model_name='porttemplatemapping', + constraint=models.UniqueConstraint( + fields=('rear_port', 'rear_port_position'), + name='dcim_porttemplatemapping_unique_rear_port_position' + ), + ), + + # Create PortMapping model (for Devices) + migrations.CreateModel( + name='PortMapping', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ( + 'front_port_position', + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024) + ] + ), + ), + ( + 'rear_port_position', + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024), + ] + ), + ), + ( + 'device', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.device', + related_name='port_mappings' + ) + ), + ( + 'front_port', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.frontport', + related_name='mappings' + ) + ), + ( + 'rear_port', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='dcim.rearport', + related_name='mappings' + ) + ), + ], + ), + migrations.AddConstraint( + model_name='portmapping', + constraint=models.UniqueConstraint( + fields=('front_port', 'front_port_position'), + name='dcim_portmapping_unique_front_port_position' + ), + ), + migrations.AddConstraint( + model_name='portmapping', + constraint=models.UniqueConstraint( + fields=('rear_port', 'rear_port_position'), + name='dcim_portmapping_unique_rear_port_position' + ), + ), + + # Data migration + migrations.RunPython( + code=populate_port_template_mappings, + reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=populate_port_mappings, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0223_frontport_positions.py b/netbox/dcim/migrations/0223_frontport_positions.py new file mode 100644 index 000000000..fc3394738 --- /dev/null +++ b/netbox/dcim/migrations/0223_frontport_positions.py @@ -0,0 +1,65 @@ +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0222_port_mappings'), + ] + + operations = [ + # Remove rear_port & rear_port_position from FrontPortTemplate + migrations.RemoveConstraint( + model_name='frontporttemplate', + name='dcim_frontporttemplate_unique_rear_port_position', + ), + migrations.RemoveField( + model_name='frontporttemplate', + name='rear_port', + ), + migrations.RemoveField( + model_name='frontporttemplate', + name='rear_port_position', + ), + + # Add positions on FrontPortTemplate + migrations.AddField( + model_name='frontporttemplate', + name='positions', + field=models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024) + ] + ), + ), + + # Remove rear_port & rear_port_position from FrontPort + migrations.RemoveConstraint( + model_name='frontport', + name='dcim_frontport_unique_rear_port_position', + ), + migrations.RemoveField( + model_name='frontport', + name='rear_port', + ), + migrations.RemoveField( + model_name='frontport', + name='rear_port_position', + ), + + # Add positions on FrontPort + migrations.AddField( + model_name='frontport', + name='positions', + field=models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1024) + ] + ), + ), + ] diff --git a/netbox/dcim/models/base.py b/netbox/dcim/models/base.py new file mode 100644 index 000000000..f8021d4db --- /dev/null +++ b/netbox/dcim/models/base.py @@ -0,0 +1,61 @@ +from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from dcim.constants import PORT_POSITION_MAX, PORT_POSITION_MIN + +__all__ = ( + 'PortMappingBase', +) + + +class PortMappingBase(models.Model): + """ + Base class for PortMapping and PortTemplateMapping + """ + front_port_position = models.PositiveSmallIntegerField( + default=1, + validators=( + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX), + ), + ) + rear_port_position = models.PositiveSmallIntegerField( + default=1, + validators=( + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX), + ), + ) + + _netbox_private = True + + class Meta: + abstract = True + constraints = ( + models.UniqueConstraint( + fields=('front_port', 'front_port_position'), + name='%(app_label)s_%(class)s_unique_front_port_position' + ), + models.UniqueConstraint( + fields=('rear_port', 'rear_port_position'), + name='%(app_label)s_%(class)s_unique_rear_port_position' + ), + ) + + def clean(self): + super().clean() + + # Validate rear port position + if self.rear_port_position > self.rear_port.positions: + raise ValidationError({ + "rear_port_position": _( + "Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} " + "positions." + ).format( + rear_port_position=self.rear_port_position, + name=self.rear_port.name, + positions=self.rear_port.positions + ) + }) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index b55b114c0..e75b4c110 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -1,4 +1,5 @@ import itertools +import logging from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -22,7 +23,7 @@ from utilities.fields import ColorField, GenericArrayForeignKey from utilities.querysets import RestrictedQuerySet from utilities.serialization import deserialize_object, serialize_object from wireless.models import WirelessLink -from .device_components import FrontPort, PathEndpoint, RearPort +from .device_components import FrontPort, PathEndpoint, PortMapping, RearPort __all__ = ( 'Cable', @@ -30,6 +31,8 @@ __all__ = ( 'CableTermination', ) +logger = logging.getLogger(f'netbox.{__name__}') + trace_paths = Signal() @@ -666,7 +669,13 @@ class CablePath(models.Model): is_active = True is_split = False + logger.debug(f'Tracing cable path from {terminations}...') + + segment = 0 while terminations: + segment += 1 + logger.debug(f'[Path segment #{segment}] Position stack: {position_stack}') + logger.debug(f'[Path segment #{segment}] Local terminations: {terminations}') # Terminations must all be of the same type if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]): @@ -697,7 +706,10 @@ class CablePath(models.Model): position_stack.append([terminations[0].cable_position]) # Step 2: Determine the attached links (Cable or WirelessLink), if any - links = [termination.link for termination in terminations if termination.link is not None] + links = list(dict.fromkeys( + termination.link for termination in terminations if termination.link is not None + )) + logger.debug(f'[Path segment #{segment}] Links: {links}') if len(links) == 0: if len(path) == 1: # If this is the start of the path and no link exists, return None @@ -760,10 +772,13 @@ class CablePath(models.Model): link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links ] + logger.debug(f'[Path segment #{segment}] Remote terminations: {remote_terminations}') + # Remote Terminations must all be of the same type, otherwise return a split path if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]): is_complete = False is_split = True + logger.debug('Remote termination types differ; aborting trace.') break # Step 7: Record the far-end termination object(s) @@ -777,58 +792,53 @@ class CablePath(models.Model): if isinstance(remote_terminations[0], FrontPort): # Follow FrontPorts to their corresponding RearPorts - rear_ports = RearPort.objects.filter( - pk__in=[t.rear_port_id for t in remote_terminations] - ) - if len(rear_ports) > 1 or rear_ports[0].positions > 1: - position_stack.append([fp.rear_port_position for fp in remote_terminations]) - - terminations = rear_ports - - elif isinstance(remote_terminations[0], RearPort): - if len(remote_terminations) == 1 and remote_terminations[0].positions == 1: - front_ports = FrontPort.objects.filter( - rear_port_id__in=[rp.pk for rp in remote_terminations], - rear_port_position=1 - ) - # Obtain the individual front ports based on the termination and all positions - elif len(remote_terminations) > 1 and position_stack: + if remote_terminations[0].positions > 1 and position_stack: positions = position_stack.pop() - - # Ensure we have a number of positions equal to the amount of remote terminations - if len(remote_terminations) != len(positions): - raise UnsupportedCablePath( - _("All positions counts within the path on opposite ends of links must match") - ) - - # Get our front ports q_filter = Q() for rt in remote_terminations: - position = positions.pop() - q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position) - if q_filter is Q(): - raise UnsupportedCablePath(_("Remote termination position filter is missing")) - front_ports = FrontPort.objects.filter(q_filter) - # Obtain the individual front ports based on the termination and position - elif position_stack: - front_ports = FrontPort.objects.filter( - rear_port_id=remote_terminations[0].pk, - rear_port_position__in=position_stack.pop() - ) - # If all rear ports have a single position, we can just get the front ports - elif all([rp.positions == 1 for rp in remote_terminations]): - front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations]) - - if len(front_ports) != len(remote_terminations): - # Some rear ports does not have a front port - is_split = True - break - else: - # No position indicated: path has split, so we stop at the RearPorts + q_filter |= Q(front_port=rt, front_port_position__in=positions) + port_mappings = PortMapping.objects.filter(q_filter) + elif remote_terminations[0].positions > 1: is_split = True + logger.debug( + 'Encountered front port mapped to multiple rear ports but position stack is empty; aborting ' + 'trace.' + ) + break + else: + port_mappings = PortMapping.objects.filter(front_port__in=remote_terminations) + if not port_mappings: break - terminations = front_ports + # Compile the list of RearPorts without duplication or altering their ordering + terminations = list(dict.fromkeys(mapping.rear_port for mapping in port_mappings)) + if any(t.positions > 1 for t in terminations): + position_stack.append([mapping.rear_port_position for mapping in port_mappings]) + + elif isinstance(remote_terminations[0], RearPort): + # Follow RearPorts to their corresponding FrontPorts + if remote_terminations[0].positions > 1 and position_stack: + positions = position_stack.pop() + q_filter = Q() + for rt in remote_terminations: + q_filter |= Q(rear_port=rt, rear_port_position__in=positions) + port_mappings = PortMapping.objects.filter(q_filter) + elif remote_terminations[0].positions > 1: + is_split = True + logger.debug( + 'Encountered rear port mapped to multiple front ports but position stack is empty; aborting ' + 'trace.' + ) + break + else: + port_mappings = PortMapping.objects.filter(rear_port__in=remote_terminations) + if not port_mappings: + break + + # Compile the list of FrontPorts without duplication or altering their ordering + terminations = list(dict.fromkeys(mapping.front_port for mapping in port_mappings)) + if any(t.positions > 1 for t in terminations): + position_stack.append([mapping.front_port_position for mapping in port_mappings]) elif isinstance(remote_terminations[0], CircuitTermination): # Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa) @@ -876,6 +886,7 @@ class CablePath(models.Model): # Unsupported topology, mark as split and exit is_complete = False is_split = True + logger.warning('Encountered an unsupported topology; aborting trace.') break return cls( @@ -954,16 +965,23 @@ class CablePath(models.Model): # RearPort splitting to multiple FrontPorts with no stack position if type(nodes[0]) is RearPort: - return FrontPort.objects.filter(rear_port__in=nodes) + return [ + mapping.front_port for mapping in + PortMapping.objects.filter(rear_port__in=nodes).prefetch_related('front_port') + ] # Cable terminating to multiple FrontPorts mapped to different # RearPorts connected to different cables - elif type(nodes[0]) is FrontPort: - return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes]) + if type(nodes[0]) is FrontPort: + return [ + mapping.rear_port for mapping in + PortMapping.objects.filter(front_port__in=nodes).prefetch_related('rear_port') + ] # Cable terminating to multiple CircuitTerminations - elif type(nodes[0]) is CircuitTermination: + if type(nodes[0]) is CircuitTermination: return [ ct.get_peer_termination() for ct in nodes ] + return [] def get_asymmetric_nodes(self): """ diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 74e624d6c..16e281523 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -7,6 +7,7 @@ from mptt.models import MPTTModel, TreeForeignKey from dcim.choices import * from dcim.constants import * +from dcim.models.base import PortMappingBase from dcim.models.mixins import InterfaceValidationMixin from netbox.models import ChangeLoggedModel from utilities.fields import ColorField, NaturalOrderingField @@ -28,6 +29,7 @@ __all__ = ( 'InterfaceTemplate', 'InventoryItemTemplate', 'ModuleBayTemplate', + 'PortTemplateMapping', 'PowerOutletTemplate', 'PowerPortTemplate', 'RearPortTemplate', @@ -518,6 +520,53 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel) } +class PortTemplateMapping(PortMappingBase): + """ + Maps a FrontPortTemplate & position to a RearPortTemplate & position. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='port_mappings', + blank=True, + null=True, + ) + module_type = models.ForeignKey( + to='dcim.ModuleType', + on_delete=models.CASCADE, + related_name='port_mappings', + blank=True, + null=True, + ) + front_port = models.ForeignKey( + to='dcim.FrontPortTemplate', + on_delete=models.CASCADE, + related_name='mappings', + ) + rear_port = models.ForeignKey( + to='dcim.RearPortTemplate', + on_delete=models.CASCADE, + related_name='mappings', + ) + + def clean(self): + super().clean() + + # Validate rear port assignment + if self.front_port.device_type_id != self.rear_port.device_type_id: + raise ValidationError({ + "rear_port": _("Rear port ({rear_port}) must belong to the same device type").format( + rear_port=self.rear_port + ) + }) + + def save(self, *args, **kwargs): + # Associate the mapping with the parent DeviceType/ModuleType + self.device_type = self.front_port.device_type + self.module_type = self.front_port.module_type + super().save(*args, **kwargs) + + class FrontPortTemplate(ModularComponentTemplateModel): """ Template for a pass-through port on the front of a new Device. @@ -531,18 +580,13 @@ class FrontPortTemplate(ModularComponentTemplateModel): verbose_name=_('color'), blank=True ) - rear_port = models.ForeignKey( - to='dcim.RearPortTemplate', - on_delete=models.CASCADE, - related_name='frontport_templates' - ) - rear_port_position = models.PositiveSmallIntegerField( - verbose_name=_('rear port position'), + positions = models.PositiveSmallIntegerField( + verbose_name=_('positions'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) - ] + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) + ], ) component_model = FrontPort @@ -557,10 +601,6 @@ class FrontPortTemplate(ModularComponentTemplateModel): fields=('module_type', 'name'), name='%(app_label)s_%(class)s_unique_module_type_name' ), - models.UniqueConstraint( - fields=('rear_port', 'rear_port_position'), - name='%(app_label)s_%(class)s_unique_rear_port_position' - ), ) verbose_name = _('front port template') verbose_name_plural = _('front port templates') @@ -568,40 +608,23 @@ class FrontPortTemplate(ModularComponentTemplateModel): def clean(self): super().clean() - try: - - # Validate rear port assignment - if self.rear_port.device_type != self.device_type: - raise ValidationError( - _("Rear port ({name}) must belong to the same device type").format(name=self.rear_port) - ) - - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError( - _("Invalid rear port position ({position}); rear port {name} has only {count} positions").format( - position=self.rear_port_position, - name=self.rear_port.name, - count=self.rear_port.positions - ) - ) - - except RearPortTemplate.DoesNotExist: - pass + # Check that positions is greater than or equal to the number of associated RearPortTemplates + if not self._state.adding: + mapping_count = self.mappings.count() + if self.positions < mapping_count: + raise ValidationError({ + "positions": _( + "The number of positions cannot be less than the number of mapped rear port templates ({count})" + ).format(count=mapping_count) + }) def instantiate(self, **kwargs): - if self.rear_port: - rear_port_name = self.rear_port.resolve_name(kwargs.get('module')) - rear_port = RearPort.objects.get(name=rear_port_name, **kwargs) - else: - rear_port = None return self.component_model( name=self.resolve_name(kwargs.get('module')), label=self.resolve_label(kwargs.get('module')), type=self.type, color=self.color, - rear_port=rear_port, - rear_port_position=self.rear_port_position, + positions=self.positions, **kwargs ) instantiate.do_not_call_in_templates = True @@ -611,8 +634,7 @@ class FrontPortTemplate(ModularComponentTemplateModel): 'name': self.name, 'type': self.type, 'color': self.color, - 'rear_port': self.rear_port.name, - 'rear_port_position': self.rear_port_position, + 'positions': self.positions, 'label': self.label, 'description': self.description, } @@ -635,9 +657,9 @@ class RearPortTemplate(ModularComponentTemplateModel): verbose_name=_('positions'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) - ] + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) + ], ) component_model = RearPort @@ -646,6 +668,20 @@ class RearPortTemplate(ModularComponentTemplateModel): verbose_name = _('rear port template') verbose_name_plural = _('rear port templates') + def clean(self): + super().clean() + + # Check that positions is greater than or equal to the number of associated FrontPortTemplates + if not self._state.adding: + mapping_count = self.mappings.count() + if self.positions < mapping_count: + raise ValidationError({ + "positions": _( + "The number of positions cannot be less than the number of mapped front port templates " + "({count})" + ).format(count=mapping_count) + }) + def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 8c9acc48f..e2077e9fe 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey from dcim.choices import * from dcim.constants import * from dcim.fields import WWNField +from dcim.models.base import PortMappingBase from dcim.models.mixins import InterfaceValidationMixin from netbox.choices import ColorChoices from netbox.models import OrganizationalModel, NetBoxModel @@ -35,6 +36,7 @@ __all__ = ( 'InventoryItemRole', 'ModuleBay', 'PathEndpoint', + 'PortMapping', 'PowerOutlet', 'PowerPort', 'RearPort', @@ -208,10 +210,6 @@ class CabledObjectModel(models.Model): raise ValidationError({ "cable_end": _("Must specify cable end (A or B) when attaching a cable.") }) - if not self.cable_position: - raise ValidationError({ - "cable_position": _("Must specify cable termination position when attaching a cable.") - }) if self.cable_end and not self.cable: raise ValidationError({ "cable_end": _("Cable end must not be set without a cable.") @@ -1069,6 +1067,43 @@ class Interface( # Pass-through ports # +class PortMapping(PortMappingBase): + """ + Maps a FrontPort & position to a RearPort & position. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='port_mappings', + ) + front_port = models.ForeignKey( + to='dcim.FrontPort', + on_delete=models.CASCADE, + related_name='mappings', + ) + rear_port = models.ForeignKey( + to='dcim.RearPort', + on_delete=models.CASCADE, + related_name='mappings', + ) + + def clean(self): + super().clean() + + # Both ports must belong to the same device + if self.front_port.device_id != self.rear_port.device_id: + raise ValidationError({ + "rear_port": _("Rear port ({rear_port}) must belong to the same device").format( + rear_port=self.rear_port + ) + }) + + def save(self, *args, **kwargs): + # Associate the mapping with the parent Device + self.device = self.front_port.device + super().save(*args, **kwargs) + + class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): """ A pass-through port on the front of a Device. @@ -1082,22 +1117,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): verbose_name=_('color'), blank=True ) - rear_port = models.ForeignKey( - to='dcim.RearPort', - on_delete=models.CASCADE, - related_name='frontports' - ) - rear_port_position = models.PositiveSmallIntegerField( - verbose_name=_('rear port position'), + positions = models.PositiveSmallIntegerField( + verbose_name=_('positions'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) ], - help_text=_('Mapped position on corresponding rear port') ) - clone_fields = ('device', 'type', 'color') + clone_fields = ('device', 'type', 'color', 'positions') class Meta(ModularComponentModel.Meta): constraints = ( @@ -1105,10 +1134,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): fields=('device', 'name'), name='%(app_label)s_%(class)s_unique_device_name' ), - models.UniqueConstraint( - fields=('rear_port', 'rear_port_position'), - name='%(app_label)s_%(class)s_unique_rear_port_position' - ), ) verbose_name = _('front port') verbose_name_plural = _('front ports') @@ -1116,27 +1141,14 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): def clean(self): super().clean() - if hasattr(self, 'rear_port'): - - # Validate rear port assignment - if self.rear_port.device != self.device: + # Check that positions is greater than or equal to the number of associated RearPorts + if not self._state.adding: + mapping_count = self.mappings.count() + if self.positions < mapping_count: raise ValidationError({ - "rear_port": _( - "Rear port ({rear_port}) must belong to the same device" - ).format(rear_port=self.rear_port) - }) - - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError({ - "rear_port_position": _( - "Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} " - "positions." - ).format( - rear_port_position=self.rear_port_position, - name=self.rear_port.name, - positions=self.rear_port.positions - ) + "positions": _( + "The number of positions cannot be less than the number of mapped rear ports ({count})" + ).format(count=mapping_count) }) @@ -1157,11 +1169,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): verbose_name=_('positions'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) ], - help_text=_('Number of front ports which may be mapped') ) + clone_fields = ('device', 'type', 'color', 'positions') class Meta(ModularComponentModel.Meta): @@ -1173,13 +1185,13 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): # Check that positions count is greater than or equal to the number of associated FrontPorts if not self._state.adding: - frontport_count = self.frontports.count() - if self.positions < frontport_count: + mapping_count = self.mappings.count() + if self.positions < mapping_count: raise ValidationError({ "positions": _( "The number of positions cannot be less than the number of mapped front ports " - "({frontport_count})" - ).format(frontport_count=frontport_count) + "({count})" + ).format(count=mapping_count) }) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 5e67f1ea8..423265751 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,8 +1,7 @@ import decimal -import yaml - from functools import cached_property +import yaml from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -19,14 +18,14 @@ from django.utils.translation import gettext_lazy as _ from dcim.choices import * from dcim.constants import * from dcim.fields import MACAddressField -from dcim.utils import update_interface_bridges +from dcim.utils import create_port_mappings, update_interface_bridges from extras.models import ConfigContextModel, CustomField from extras.querysets import ConfigContextModelQuerySet from netbox.choices import ColorChoices from netbox.config import ConfigItem from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel -from netbox.models.mixins import WeightMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin +from netbox.models.mixins import WeightMixin from utilities.fields import ColorField, CounterCacheField from utilities.prefetch import get_prefetchable_fields from utilities.tracking import TrackingModelMixin @@ -34,7 +33,6 @@ from .device_components import * from .mixins import RenderConfigMixin from .modules import Module - __all__ = ( 'Device', 'DeviceRole', @@ -1003,6 +1001,8 @@ class Device( self._instantiate_components(self.device_type.interfacetemplates.all()) self._instantiate_components(self.device_type.rearporttemplates.all()) self._instantiate_components(self.device_type.frontporttemplates.all()) + # Replicate any front/rear port mappings from the DeviceType + create_port_mappings(self, self.device_type) # Disable bulk_create to accommodate MPTT self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False) self._instantiate_components(self.device_type.devicebaytemplates.all()) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index eb1825c1a..5ec1f68d7 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -1,5 +1,6 @@ import logging +from django.db.models import Q from django.db.models.signals import post_save, post_delete from django.dispatch import receiver @@ -7,7 +8,7 @@ from dcim.choices import CableEndChoices, LinkStatusChoices from virtualization.models import VMInterface from .models import ( Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface, - InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location, + InventoryItem, ModuleBay, PathEndpoint, PortMapping, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location, VirtualChassis, ) from .models.cables import trace_paths @@ -135,6 +136,17 @@ def retrace_cable_paths(instance, **kwargs): cablepath.retrace() +@receiver((post_delete, post_save), sender=PortMapping) +def update_passthrough_port_paths(instance, **kwargs): + """ + When a PortMapping is created or deleted, retrace any CablePaths which traverse its front and/or rear ports. + """ + for cablepath in CablePath.objects.filter( + Q(_nodes__contains=instance.front_port) | Q(_nodes__contains=instance.rear_port) + ): + cablepath.retrace() + + @receiver(post_delete, sender=CableTermination) def nullify_connected_endpoints(instance, **kwargs): """ @@ -150,17 +162,6 @@ def nullify_connected_endpoints(instance, **kwargs): cablepath.retrace() -@receiver(post_save, sender=FrontPort) -def extend_rearport_cable_paths(instance, created, raw, **kwargs): - """ - When a new FrontPort is created, add it to any CablePaths which end at its corresponding RearPort. - """ - if created and not raw: - rearport = instance.rear_port - for cablepath in CablePath.objects.filter(_nodes__contains=rearport): - cablepath.retrace() - - @receiver(post_save, sender=Interface) @receiver(post_save, sender=VMInterface) def update_mac_address_interface(instance, created, raw, **kwargs): diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 5f9467297..d81718d78 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -749,12 +749,9 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable): color = columns.ColorColumn( verbose_name=_('Color'), ) - rear_port_position = tables.Column( - verbose_name=_('Position') - ) - rear_port = tables.Column( - verbose_name=_('Rear Port'), - linkify=True + mappings = columns.ManyToManyColumn( + verbose_name=_('Mappings'), + transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}' ) tags = columns.TagColumn( url_name='dcim:frontport_list' @@ -763,12 +760,12 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable): class Meta(DeviceComponentTable.Meta): model = models.FrontPort fields = ( - 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', - 'inventory_items', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', + 'last_updated', ) default_columns = ( - 'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', + 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description', ) @@ -786,11 +783,11 @@ class DeviceFrontPortTable(FrontPortTable): class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): model = models.FrontPort fields = ( - 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position', + 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer', + 'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer', ) @@ -805,6 +802,10 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable): color = columns.ColorColumn( verbose_name=_('Color'), ) + mappings = columns.ManyToManyColumn( + verbose_name=_('Mappings'), + transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}' + ) tags = columns.TagColumn( url_name='dcim:rearport_list' ) @@ -812,10 +813,13 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable): class Meta(DeviceComponentTable.Meta): model = models.RearPort fields = ( - 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', + 'last_updated', + ) + default_columns = ( + 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'mappings', 'description', ) - default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description') class DeviceRearPortTable(RearPortTable): @@ -832,11 +836,11 @@ class DeviceRearPortTable(RearPortTable): class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): model = models.RearPort fields = ( - 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected', - 'cable', 'cable_color', 'link_peer', 'tags', 'actions', + 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'mappings', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', + 'pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'cable', 'link_peer', ) diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 979689b75..b7ad758df 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -249,12 +249,13 @@ class InterfaceTemplateTable(ComponentTemplateTable): class FrontPortTemplateTable(ComponentTemplateTable): - rear_port_position = tables.Column( - verbose_name=_('Position') - ) color = columns.ColorColumn( verbose_name=_('Color'), ) + mappings = columns.ManyToManyColumn( + verbose_name=_('Mappings'), + transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}' + ) actions = columns.ActionsColumn( actions=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS @@ -262,7 +263,7 @@ class FrontPortTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = models.FrontPortTemplate - fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions') + fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'actions') empty_text = "None" @@ -270,6 +271,10 @@ class RearPortTemplateTable(ComponentTemplateTable): color = columns.ColorColumn( verbose_name=_('Color'), ) + mappings = columns.ManyToManyColumn( + verbose_name=_('Mappings'), + transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}' + ) actions = columns.ActionsColumn( actions=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS @@ -277,7 +282,7 @@ class RearPortTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = models.RearPortTemplate - fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions') + fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'mappings', 'description', 'actions') empty_text = "None" diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index bdade5395..d4783bc3c 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -973,72 +973,99 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase): RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C), RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C), RearPortTemplate(device_type=devicetype, name='Rear Port Template 4', type=PortTypeChoices.TYPE_8P8C), - RearPortTemplate(module_type=moduletype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C), - RearPortTemplate(module_type=moduletype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C), - RearPortTemplate(module_type=moduletype, name='Rear Port Template 7', type=PortTypeChoices.TYPE_8P8C), - RearPortTemplate(module_type=moduletype, name='Rear Port Template 8', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 5', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 6', type=PortTypeChoices.TYPE_8P8C), ) RearPortTemplate.objects.bulk_create(rear_port_templates) - front_port_templates = ( - FrontPortTemplate( - device_type=devicetype, - name='Front Port Template 1', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_port_templates[0] - ), - FrontPortTemplate( - device_type=devicetype, - name='Front Port Template 2', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_port_templates[1] - ), - FrontPortTemplate( - module_type=moduletype, - name='Front Port Template 5', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_port_templates[4] - ), - FrontPortTemplate( - module_type=moduletype, - name='Front Port Template 6', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_port_templates[5] - ), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C), ) FrontPortTemplate.objects.bulk_create(front_port_templates) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping( + device_type=devicetype, + front_port=front_port_templates[0], + rear_port=rear_port_templates[0], + ), + PortTemplateMapping( + device_type=devicetype, + front_port=front_port_templates[1], + rear_port=rear_port_templates[1], + ), + PortTemplateMapping( + module_type=moduletype, + front_port=front_port_templates[2], + rear_port=rear_port_templates[2], + ), + ]) cls.create_data = [ { 'device_type': devicetype.pk, 'name': 'Front Port Template 3', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_port_templates[2].pk, - 'rear_port_position': 1, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_port_templates[3].pk, + 'rear_port_position': 1, + }, + ], }, { 'device_type': devicetype.pk, 'name': 'Front Port Template 4', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_port_templates[3].pk, - 'rear_port_position': 1, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_port_templates[4].pk, + 'rear_port_position': 1, + }, + ], }, { 'module_type': moduletype.pk, 'name': 'Front Port Template 7', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_port_templates[6].pk, - 'rear_port_position': 1, - }, - { - 'module_type': moduletype.pk, - 'name': 'Front Port Template 8', - 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_port_templates[7].pk, - 'rear_port_position': 1, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_port_templates[5].pk, + 'rear_port_position': 1, + }, + ], }, ] + cls.update_data = { + 'type': PortTypeChoices.TYPE_LC, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_port_templates[3].pk, + 'rear_port_position': 1, + }, + ], + } + + def test_update_object(self): + super().test_update_object() + + # Check that the port mapping was updated after modifying the front port template + front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 1') + rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 4') + self.assertTrue( + PortTemplateMapping.objects.filter( + front_port=front_port_template, + front_port_position=1, + rear_port=rear_port_template, + rear_port_position=1, + ).exists() + ) + class RearPortTemplateTest(APIViewTestCases.APIViewTestCase): model = RearPortTemplate @@ -1057,36 +1084,104 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase): manufacturer=manufacturer, model='Module Type 1' ) + front_port_templates = ( + FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(module_type=moduletype, name='Front Port Template 3', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(module_type=moduletype, name='Front Port Template 4', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(module_type=moduletype, name='Front Port Template 5', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(module_type=moduletype, name='Front Port Template 6', type=PortTypeChoices.TYPE_8P8C), + ) + FrontPortTemplate.objects.bulk_create(front_port_templates) rear_port_templates = ( RearPortTemplate(device_type=devicetype, name='Rear Port Template 1', type=PortTypeChoices.TYPE_8P8C), RearPortTemplate(device_type=devicetype, name='Rear Port Template 2', type=PortTypeChoices.TYPE_8P8C), RearPortTemplate(device_type=devicetype, name='Rear Port Template 3', type=PortTypeChoices.TYPE_8P8C), ) RearPortTemplate.objects.bulk_create(rear_port_templates) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping( + device_type=devicetype, + front_port=front_port_templates[0], + rear_port=rear_port_templates[0], + ), + PortTemplateMapping( + device_type=devicetype, + front_port=front_port_templates[1], + rear_port=rear_port_templates[1], + ), + PortTemplateMapping( + module_type=moduletype, + front_port=front_port_templates[2], + rear_port=rear_port_templates[2], + ), + ]) cls.create_data = [ { 'device_type': devicetype.pk, 'name': 'Rear Port Template 4', 'type': PortTypeChoices.TYPE_8P8C, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_port_templates[3].pk, + 'front_port_position': 1, + }, + ], }, { 'device_type': devicetype.pk, 'name': 'Rear Port Template 5', 'type': PortTypeChoices.TYPE_8P8C, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_port_templates[4].pk, + 'front_port_position': 1, + }, + ], }, { 'module_type': moduletype.pk, 'name': 'Rear Port Template 6', 'type': PortTypeChoices.TYPE_8P8C, - }, - { - 'module_type': moduletype.pk, - 'name': 'Rear Port Template 7', - 'type': PortTypeChoices.TYPE_8P8C, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_port_templates[5].pk, + 'front_port_position': 1, + }, + ], }, ] + cls.update_data = { + 'type': PortTypeChoices.TYPE_LC, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_port_templates[3].pk, + 'front_port_position': 1, + }, + ], + } + + def test_update_object(self): + super().test_update_object() + + # Check that the port mapping was updated after modifying the rear port template + front_port_template = FrontPortTemplate.objects.get(name='Front Port Template 4') + rear_port_template = RearPortTemplate.objects.get(name='Rear Port Template 1') + self.assertTrue( + PortTemplateMapping.objects.filter( + front_port=front_port_template, + front_port_position=1, + rear_port=rear_port_template, + rear_port_position=1, + ).exists() + ) + class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase): model = ModuleBayTemplate @@ -2015,51 +2110,90 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase): RearPort(device=device, name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C), ) RearPort.objects.bulk_create(rear_ports) - front_ports = ( - FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), - FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), - FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[2]), + FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C), ) FrontPort.objects.bulk_create(front_ports) + PortMapping.objects.bulk_create([ + PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]), + PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]), + PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]), + ]) cls.create_data = [ { 'device': device.pk, 'name': 'Front Port 4', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_ports[3].pk, - 'rear_port_position': 1, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_ports[3].pk, + 'rear_port_position': 1, + }, + ], }, { 'device': device.pk, 'name': 'Front Port 5', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_ports[4].pk, - 'rear_port_position': 1, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_ports[4].pk, + 'rear_port_position': 1, + }, + ], }, { 'device': device.pk, 'name': 'Front Port 6', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rear_ports[5].pk, - 'rear_port_position': 1, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_ports[5].pk, + 'rear_port_position': 1, + }, + ], }, ] + cls.update_data = { + 'type': PortTypeChoices.TYPE_LC, + 'rear_ports': [ + { + 'position': 1, + 'rear_port': rear_ports[3].pk, + 'rear_port_position': 1, + }, + ], + } + + def test_update_object(self): + super().test_update_object() + + # Check that the port mapping was updated after modifying the front port + front_port = FrontPort.objects.get(name='Front Port 1') + rear_port = RearPort.objects.get(name='Rear Port 4') + self.assertTrue( + PortMapping.objects.filter( + front_port=front_port, + front_port_position=1, + rear_port=rear_port, + rear_port_position=1, + ).exists() + ) + @tag('regression') # Issue #18991 def test_front_port_paths(self): device = Device.objects.first() - rear_port = RearPort.objects.create( - device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C - ) interface1 = Interface.objects.create(device=device, name='Interface 1') - front_port = FrontPort.objects.create( - device=device, - name='Rear Port 10', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_port, - ) + rear_port = RearPort.objects.create(device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C) + front_port = FrontPort.objects.create(device=device, name='Front Port 10', type=PortTypeChoices.TYPE_8P8C) + PortMapping.objects.create(device=device, front_port=front_port, rear_port=rear_port) Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port]) self.add_permissions(f'dcim.view_{self.model._meta.model_name}') @@ -2086,6 +2220,15 @@ class RearPortTest(APIViewTestCases.APIViewTestCase): role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000') device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site) + front_ports = ( + FrontPort(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 3', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 4', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 5', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=device, name='Front Port 6', type=PortTypeChoices.TYPE_8P8C), + ) + FrontPort.objects.bulk_create(front_ports) rear_ports = ( RearPort(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), RearPort(device=device, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), @@ -2098,19 +2241,66 @@ class RearPortTest(APIViewTestCases.APIViewTestCase): 'device': device.pk, 'name': 'Rear Port 4', 'type': PortTypeChoices.TYPE_8P8C, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_ports[3].pk, + 'front_port_position': 1, + }, + ], }, { 'device': device.pk, 'name': 'Rear Port 5', 'type': PortTypeChoices.TYPE_8P8C, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_ports[4].pk, + 'front_port_position': 1, + }, + ], }, { 'device': device.pk, 'name': 'Rear Port 6', 'type': PortTypeChoices.TYPE_8P8C, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_ports[5].pk, + 'front_port_position': 1, + }, + ], }, ] + cls.update_data = { + 'type': PortTypeChoices.TYPE_LC, + 'front_ports': [ + { + 'position': 1, + 'front_port': front_ports[3].pk, + 'front_port_position': 1, + }, + ], + } + + def test_update_object(self): + super().test_update_object() + + # Check that the port mapping was updated after modifying the rear port + front_port = FrontPort.objects.get(name='Front Port 4') + rear_port = RearPort.objects.get(name='Rear Port 1') + self.assertTrue( + PortMapping.objects.filter( + front_port=front_port, + front_port_position=1, + rear_port=rear_port, + rear_port_position=1, + ).exists() + ) + @tag('regression') # Issue #18991 def test_rear_port_paths(self): device = Device.objects.first() diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index ed31dd0f4..1bd613e3b 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -281,9 +281,14 @@ class LegacyCablePathTests(CablePathTestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + PortMapping.objects.create( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1 ) # Create cable 1 @@ -340,9 +345,14 @@ class LegacyCablePathTests(CablePathTestCase): interface2 = Interface.objects.create(device=self.device, name='Interface 2') interface3 = Interface.objects.create(device=self.device, name='Interface 3') interface4 = Interface.objects.create(device=self.device, name='Interface 4') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + PortMapping.objects.create( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1 ) # Create cable 1 @@ -403,18 +413,40 @@ class LegacyCablePathTests(CablePathTestCase): interface4 = Interface.objects.create(device=self.device, name='Interface 4') rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 - ) - frontport2_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') + frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2_1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2_2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=2, + ), + ]) # Create cables 1-2 cable1 = Cable( @@ -521,18 +553,40 @@ class LegacyCablePathTests(CablePathTestCase): interface8 = Interface.objects.create(device=self.device, name='Interface 8') rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 - ) - frontport2_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') + frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2_1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2_2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=2, + ), + ]) # Create cables 1-2 cable1 = Cable( @@ -680,27 +734,59 @@ class LegacyCablePathTests(CablePathTestCase): interface3 = Interface.objects.create(device=self.device, name='Interface 3') interface4 = Interface.objects.create(device=self.device, name='Interface 4') rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3') rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) - frontport3 = FrontPort.objects.create( - device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 - ) - frontport4_1 = FrontPort.objects.create( - device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1 - ) - frontport4_2 = FrontPort.objects.create( - device=self.device, name='Front Port 4:2', rear_port=rearport4, rear_port_position=2 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3') + frontport4_1 = FrontPort.objects.create(device=self.device, name='Front Port 4:1') + frontport4_2 = FrontPort.objects.create(device=self.device, name='Front Port 4:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4_1, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4_2, + front_port_position=1, + rear_port=rearport4, + rear_port_position=2, + ), + ]) # Create cables 1-2, 6-7 cable1 = Cable( @@ -801,30 +887,72 @@ class LegacyCablePathTests(CablePathTestCase): rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4) rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 - ) - frontport2_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 - ) - frontport3_1 = FrontPort.objects.create( - device=self.device, name='Front Port 3:1', rear_port=rearport3, rear_port_position=1 - ) - frontport3_2 = FrontPort.objects.create( - device=self.device, name='Front Port 3:2', rear_port=rearport3, rear_port_position=2 - ) - frontport4_1 = FrontPort.objects.create( - device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1 - ) - frontport4_2 = FrontPort.objects.create( - device=self.device, name='Front Port 4:2', rear_port=rearport4, rear_port_position=2 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') + frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + frontport3_1 = FrontPort.objects.create(device=self.device, name='Front Port 3:1') + frontport3_2 = FrontPort.objects.create(device=self.device, name='Front Port 3:2') + frontport4_1 = FrontPort.objects.create(device=self.device, name='Front Port 4:1') + frontport4_2 = FrontPort.objects.create(device=self.device, name='Front Port 4:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2_1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2_2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport3_1, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3_2, + front_port_position=1, + rear_port=rearport3, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport4_1, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4_2, + front_port_position=1, + rear_port=rearport4, + rear_port_position=2, + ), + ]) # Create cables 1-3, 6-8 cable1 = Cable( @@ -928,23 +1056,50 @@ class LegacyCablePathTests(CablePathTestCase): interface3 = Interface.objects.create(device=self.device, name='Interface 3') interface4 = Interface.objects.create(device=self.device, name='Interface 4') rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1) - rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 5', rear_port=rearport2, rear_port_position=1 - ) - frontport3_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport3, rear_port_position=1 - ) - frontport3_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport3, rear_port_position=2 - ) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3_1 = FrontPort.objects.create(device=self.device, name='Front Port 3:1') + frontport3_2 = FrontPort.objects.create(device=self.device, name='Front Port 3:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3_1, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3_2, + front_port_position=1, + rear_port=rearport3, + rear_port_position=2, + ), + ]) # Create cables 1-2, 5-6 cable1 = Cable( @@ -1032,13 +1187,25 @@ class LegacyCablePathTests(CablePathTestCase): interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') interface3 = Interface.objects.create(device=self.device, name='Interface 3') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=2) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + ]) # Create cables 1 cable1 = Cable( @@ -1098,10 +1265,11 @@ class LegacyCablePathTests(CablePathTestCase): [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + PortMapping.objects.create( + front_port=frontport1, front_port_position=1, rear_port=rearport1, rear_port_position=1, ) # Create cables @@ -1413,18 +1581,40 @@ class LegacyCablePathTests(CablePathTestCase): interface4 = Interface.objects.create(device=self.device, name='Interface 4') rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 - ) - frontport2_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') + frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2_1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2_2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=2, + ), + ]) circuittermination1 = CircuitTermination.objects.create( circuit=self.circuit, termination=self.site, @@ -1601,22 +1791,44 @@ class LegacyCablePathTests(CablePathTestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1) - rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) - frontport3 = FrontPort.objects.create( - device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 - ) - frontport4 = FrontPort.objects.create( - device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3') + rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3') + frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + ]) # Create cables 1-2 cable1 = Cable( @@ -1688,30 +1900,72 @@ class LegacyCablePathTests(CablePathTestCase): interface4 = Interface.objects.create(device=self.device, name='Interface 4') rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport1_3 = FrontPort.objects.create( - device=self.device, name='Front Port 1:3', rear_port=rearport1, rear_port_position=3 - ) - frontport1_4 = FrontPort.objects.create( - device=self.device, name='Front Port 1:4', rear_port=rearport1, rear_port_position=4 - ) - frontport2_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 - ) - frontport2_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 - ) - frontport2_3 = FrontPort.objects.create( - device=self.device, name='Front Port 2:3', rear_port=rearport2, rear_port_position=3 - ) - frontport2_4 = FrontPort.objects.create( - device=self.device, name='Front Port 2:4', rear_port=rearport2, rear_port_position=4 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport1_3 = FrontPort.objects.create(device=self.device, name='Front Port 1:3') + frontport1_4 = FrontPort.objects.create(device=self.device, name='Front Port 1:4') + frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') + frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + frontport2_3 = FrontPort.objects.create(device=self.device, name='Front Port 2:3') + frontport2_4 = FrontPort.objects.create(device=self.device, name='Front Port 2:4') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport1_3, + front_port_position=1, + rear_port=rearport1, + rear_port_position=3, + ), + PortMapping( + device=self.device, + front_port=frontport1_4, + front_port_position=1, + rear_port=rearport1, + rear_port_position=4, + ), + PortMapping( + device=self.device, + front_port=frontport2_1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2_2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2_3, + front_port_position=1, + rear_port=rearport2, + rear_port_position=3, + ), + PortMapping( + device=self.device, + front_port=frontport2_4, + front_port_position=1, + rear_port=rearport2, + rear_port_position=4, + ), + ]) # Create cables 1-2 cable1 = Cable( @@ -1858,22 +2112,44 @@ class LegacyCablePathTests(CablePathTestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1) - rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) - frontport3 = FrontPort.objects.create( - device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 - ) - frontport4 = FrontPort.objects.create( - device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3') + rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3') + frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + ]) cable2 = Cable( a_terminations=[rearport1], @@ -1937,22 +2213,44 @@ class LegacyCablePathTests(CablePathTestCase): interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') interface3 = Interface.objects.create(device=self.device, name='Interface 3') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1) - rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) - frontport3 = FrontPort.objects.create( - device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 - ) - frontport4 = FrontPort.objects.create( - device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3') + rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3') + frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + ]) cable2 = Cable( a_terminations=[rearport1], @@ -2033,30 +2331,62 @@ class LegacyCablePathTests(CablePathTestCase): interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') interface3 = Interface.objects.create(device=self.device, name='Interface 3') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1) - rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1) - rearport5 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1) - rearport6 = RearPort.objects.create(device=self.device, name='Rear Port 6', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) - frontport3 = FrontPort.objects.create( - device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 - ) - frontport4 = FrontPort.objects.create( - device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1 - ) - frontport5 = FrontPort.objects.create( - device=self.device, name='Front Port 5', rear_port=rearport5, rear_port_position=1 - ) - frontport6 = FrontPort.objects.create( - device=self.device, name='Front Port 6', rear_port=rearport6, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3') + rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4') + rearport5 = RearPort.objects.create(device=self.device, name='Rear Port 5') + rearport6 = RearPort.objects.create(device=self.device, name='Rear Port 6') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3') + frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4') + frontport5 = FrontPort.objects.create(device=self.device, name='Front Port 5') + frontport6 = FrontPort.objects.create(device=self.device, name='Front Port 6') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport5, + front_port_position=1, + rear_port=rearport5, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport6, + front_port_position=1, + rear_port=rearport6, + rear_port_position=1, + ), + ]) cable2 = Cable( a_terminations=[rearport1], @@ -2155,14 +2485,26 @@ class LegacyCablePathTests(CablePathTestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + ]) cable1 = Cable( a_terminations=[interface1], @@ -2274,14 +2616,26 @@ class LegacyCablePathTests(CablePathTestCase): interface2 = Interface.objects.create(device=self.device, name='Interface 2') interface3 = Interface.objects.create(device=self.device, name='Interface 3') interface4 = Interface.objects.create(device=self.device, name='Interface 4') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + ]) # Create cables cable1 = Cable( @@ -2320,14 +2674,26 @@ class LegacyCablePathTests(CablePathTestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + ]) # Create cable 2 cable2 = Cable( @@ -2373,10 +2739,17 @@ class LegacyCablePathTests(CablePathTestCase): """ interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + ]) # Create cables 1 and 2 cable1 = Cable( @@ -2478,22 +2851,44 @@ class LegacyCablePathTests(CablePathTestCase): ) interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface2 = Interface.objects.create(device=self.device, name='Interface 2') - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - rearport3 = RearPort.objects.create(device=device, name='Rear Port 3', positions=1) - rearport4 = RearPort.objects.create(device=device, name='Rear Port 4', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) - frontport3 = FrontPort.objects.create( - device=device, name='Front Port 3', rear_port=rearport3, rear_port_position=1 - ) - frontport4 = FrontPort.objects.create( - device=device, name='Front Port 4', rear_port=rearport4, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + rearport3 = RearPort.objects.create(device=device, name='Rear Port 3') + rearport4 = RearPort.objects.create(device=device, name='Rear Port 4') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + frontport3 = FrontPort.objects.create(device=self.device, name='Front Port 3') + frontport4 = FrontPort.objects.create(device=self.device, name='Front Port 4') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport3, + front_port_position=1, + rear_port=rearport3, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport4, + front_port_position=1, + rear_port=rearport4, + rear_port_position=1, + ), + ]) cable2 = Cable( a_terminations=[rearport1], diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py index c7895c6d2..0f9a704c5 100644 --- a/netbox/dcim/tests/test_cablepaths2.py +++ b/netbox/dcim/tests/test_cablepaths2.py @@ -1,5 +1,3 @@ -from unittest import skipIf - from circuits.models import CircuitTermination from dcim.choices import CableProfileChoices from dcim.models import * @@ -363,13 +361,17 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 3'), Interface.objects.create(device=self.device, name='Interface 4'), ] - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, - name='Front Port 1', - rear_port=rearport1, - rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + ]) # Create cables cable1 = Cable( @@ -439,18 +441,40 @@ class CablePathTests(CablePathTestCase): ] rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) - frontport1_1 = FrontPort.objects.create( - device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 - ) - frontport1_2 = FrontPort.objects.create( - device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 - ) - frontport2_1 = FrontPort.objects.create( - device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 - ) - frontport2_2 = FrontPort.objects.create( - device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 - ) + frontport1_1 = FrontPort.objects.create(device=self.device, name='Front Port 1:1') + frontport1_2 = FrontPort.objects.create(device=self.device, name='Front Port 1:2') + frontport2_1 = FrontPort.objects.create(device=self.device, name='Front Port 2:1') + frontport2_2 = FrontPort.objects.create(device=self.device, name='Front Port 2:2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1_1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport1_2, + front_port_position=1, + rear_port=rearport1, + rear_port_position=2, + ), + PortMapping( + device=self.device, + front_port=frontport2_1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2_2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=2, + ), + ]) # Create cables cable1 = Cable( @@ -654,25 +678,47 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 2'), ] rear_ports = [ - RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1), - RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1), - RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1), - RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1), + RearPort.objects.create(device=self.device, name='Rear Port 1'), + RearPort.objects.create(device=self.device, name='Rear Port 2'), + RearPort.objects.create(device=self.device, name='Rear Port 3'), + RearPort.objects.create(device=self.device, name='Rear Port 4'), ] front_ports = [ - FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1 - ), - FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1 - ), - FrontPort.objects.create( - device=self.device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1 - ), - FrontPort.objects.create( - device=self.device, name='Front Port 4', rear_port=rear_ports[3], rear_port_position=1 - ), + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + FrontPort.objects.create(device=self.device, name='Front Port 3'), + FrontPort.objects.create(device=self.device, name='Front Port 4'), ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[2], + front_port_position=1, + rear_port=rear_ports[2], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[3], + front_port_position=1, + rear_port=rear_ports[3], + rear_port_position=1, + ), + ]) # Create cables cable1 = Cable( @@ -723,8 +769,6 @@ class CablePathTests(CablePathTestCase): # Test SVG generation CableTraceSVG(interfaces[0]).render() - # TODO: Revisit this test under FR #20564 - @skipIf(True, "Waiting for FR #20564") def test_223_single_path_via_multiple_pass_throughs_with_breakouts(self): """ [IF1] --C1-- [FP1] [RP1] --C2-- [IF3] @@ -736,14 +780,26 @@ class CablePathTests(CablePathTestCase): Interface.objects.create(device=self.device, name='Interface 3'), Interface.objects.create(device=self.device, name='Interface 4'), ] - rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) - rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1) - frontport1 = FrontPort.objects.create( - device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1 - ) - frontport2 = FrontPort.objects.create( - device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1 - ) + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ), + ]) # Create cables cable1 = Cable( @@ -761,9 +817,6 @@ class CablePathTests(CablePathTestCase): cable2.clean() cable2.save() - for path in CablePath.objects.all(): - print(f'{path}: {path.path_objects}') - # Validate paths self.assertPathExists( (interfaces[0], cable1, [frontport1, frontport2], [rearport1, rearport2], cable2, interfaces[2]), @@ -786,3 +839,205 @@ class CablePathTests(CablePathTestCase): is_active=True ) self.assertEqual(CablePath.objects.count(), 4) + + def test_304_add_port_mapping_between_connected_ports(self): + """ + [IF1] --C1-- [FP1] [RP1] --C2-- [IF2] + """ + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + cable1 = Cable( + a_terminations=[interface1], + b_terminations=[frontport1] + ) + cable1.save() + cable2 = Cable( + a_terminations=[interface2], + b_terminations=[rearport1] + ) + cable2.save() + + # Check for incomplete paths + self.assertPathExists( + (interface1, cable1, frontport1), + is_complete=False, + is_active=True + ) + self.assertPathExists( + (interface2, cable2, rearport1), + is_complete=False, + is_active=True + ) + + # Create a PortMapping between frontport1 and rearport1 + PortMapping.objects.create( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ) + + # Check that paths are now complete + self.assertPathExists( + (interface1, cable1, frontport1, rearport1, cable2, interface2), + is_complete=True, + is_active=True + ) + self.assertPathExists( + (interface2, cable2, rearport1, frontport1, cable1, interface1), + is_complete=True, + is_active=True + ) + + def test_305_delete_port_mapping_between_connected_ports(self): + """ + [IF1] --C1-- [FP1] [RP1] --C2-- [IF2] + """ + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + cable1 = Cable( + a_terminations=[interface1], + b_terminations=[frontport1] + ) + cable1.save() + cable2 = Cable( + a_terminations=[interface2], + b_terminations=[rearport1] + ) + cable2.save() + portmapping1 = PortMapping.objects.create( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ) + + # Check for complete paths + self.assertPathExists( + (interface1, cable1, frontport1, rearport1, cable2, interface2), + is_complete=True, + is_active=True + ) + self.assertPathExists( + (interface2, cable2, rearport1, frontport1, cable1, interface1), + is_complete=True, + is_active=True + ) + + # Delete the PortMapping between frontport1 and rearport1 + portmapping1.delete() + + # Check that paths are no longer complete + self.assertPathExists( + (interface1, cable1, frontport1), + is_complete=False, + is_active=True + ) + self.assertPathExists( + (interface2, cable2, rearport1), + is_complete=False, + is_active=True + ) + + def test_306_change_port_mapping_between_connected_ports(self): + """ + [IF1] --C1-- [FP1] [RP1] --C3-- [IF3] + [IF2] --C2-- [FP2] [RP3] --C4-- [IF4] + """ + interface1 = Interface.objects.create(device=self.device, name='Interface 1') + interface2 = Interface.objects.create(device=self.device, name='Interface 2') + interface3 = Interface.objects.create(device=self.device, name='Interface 3') + interface4 = Interface.objects.create(device=self.device, name='Interface 4') + frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1') + frontport2 = FrontPort.objects.create(device=self.device, name='Front Port 2') + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1') + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2') + cable1 = Cable( + a_terminations=[interface1], + b_terminations=[frontport1] + ) + cable1.save() + cable2 = Cable( + a_terminations=[interface2], + b_terminations=[frontport2] + ) + cable2.save() + cable3 = Cable( + a_terminations=[interface3], + b_terminations=[rearport1] + ) + cable3.save() + cable4 = Cable( + a_terminations=[interface4], + b_terminations=[rearport2] + ) + cable4.save() + portmapping1 = PortMapping.objects.create( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport1, + rear_port_position=1, + ) + + # Verify expected initial paths + self.assertPathExists( + (interface1, cable1, frontport1, rearport1, cable3, interface3), + is_complete=True, + is_active=True + ) + self.assertPathExists( + (interface3, cable3, rearport1, frontport1, cable1, interface1), + is_complete=True, + is_active=True + ) + + # Delete and replace the PortMapping to connect interface1 to interface4 + portmapping1.delete() + portmapping2 = PortMapping.objects.create( + device=self.device, + front_port=frontport1, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ) + + # Verify expected new paths + self.assertPathExists( + (interface1, cable1, frontport1, rearport2, cable4, interface4), + is_complete=True, + is_active=True + ) + self.assertPathExists( + (interface4, cable4, rearport2, frontport1, cable1, interface1), + is_complete=True, + is_active=True + ) + + # Delete and replace the PortMapping to connect interface2 to interface4 + portmapping2.delete() + PortMapping.objects.create( + device=self.device, + front_port=frontport2, + front_port_position=1, + rear_port=rearport2, + rear_port_position=1, + ) + + # Verify expected new paths + self.assertPathExists( + (interface2, cable2, frontport2, rearport2, cable4, interface4), + is_complete=True, + is_active=True + ) + self.assertPathExists( + (interface4, cable4, rearport2, frontport2, cable2, interface2), + is_complete=True, + is_active=True + ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 0ba777204..bd44d4f5d 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1355,22 +1355,15 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), ) RearPortTemplate.objects.bulk_create(rear_ports) - FrontPortTemplate.objects.bulk_create( - ( - FrontPortTemplate( - device_type=device_types[0], - name='Front Port 1', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_ports[0], - ), - FrontPortTemplate( - device_type=device_types[1], - name='Front Port 2', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_ports[1], - ), - ) + front_ports = ( + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C), ) + FrontPortTemplate.objects.bulk_create(front_ports) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]), + PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]), + ]) ModuleBayTemplate.objects.bulk_create(( ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'), ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'), @@ -1626,22 +1619,15 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests): RearPortTemplate(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), ) RearPortTemplate.objects.bulk_create(rear_ports) - FrontPortTemplate.objects.bulk_create( - ( - FrontPortTemplate( - module_type=module_types[0], - name='Front Port 1', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_ports[0], - ), - FrontPortTemplate( - module_type=module_types[1], - name='Front Port 2', - type=PortTypeChoices.TYPE_8P8C, - rear_port=rear_ports[1], - ), - ) + front_ports = ( + FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C), ) + FrontPortTemplate.objects.bulk_create(front_ports) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping(module_type=module_types[0], front_port=front_ports[0], rear_port=rear_ports[0]), + PortTemplateMapping(module_type=module_types[1], front_port=front_ports[1], rear_port=rear_ports[1]), + ]) def test_q(self): params = {'q': 'foobar1'} @@ -2057,32 +2043,38 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ) RearPortTemplate.objects.bulk_create(rear_ports) - FrontPortTemplate.objects.bulk_create(( + front_ports = ( FrontPortTemplate( device_type=device_types[0], name='Front Port 1', - rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C, + positions=1, color=ColorChoices.COLOR_RED, description='foobar1' ), FrontPortTemplate( device_type=device_types[1], name='Front Port 2', - rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH, + positions=2, color=ColorChoices.COLOR_GREEN, description='foobar2' ), FrontPortTemplate( device_type=device_types[2], name='Front Port 3', - rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC, + positions=3, color=ColorChoices.COLOR_BLUE, description='foobar3' ), - )) + ) + FrontPortTemplate.objects.bulk_create(front_ports) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping(device_type=device_types[0], front_port=front_ports[0], rear_port=rear_ports[0]), + PortTemplateMapping(device_type=device_types[1], front_port=front_ports[1], rear_port=rear_ports[1]), + PortTemplateMapping(device_type=device_types[2], front_port=front_ports[2], rear_port=rear_ports[2]), + ]) def test_name(self): params = {'name': ['Front Port 1', 'Front Port 2']} @@ -2096,6 +2088,10 @@ class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_positions(self): + params = {'positions': [1, 2]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class RearPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests): queryset = RearPortTemplate.objects.all() @@ -2752,10 +2748,15 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), ) RearPort.objects.bulk_create(rear_ports) - FrontPort.objects.bulk_create(( - FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), - FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), - )) + front_ports = ( + FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C), + FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C), + ) + FrontPort.objects.bulk_create(front_ports) + PortMapping.objects.bulk_create([ + PortMapping(device=devices[0], front_port=front_ports[0], rear_port=rear_ports[0]), + PortMapping(device=devices[1], front_port=front_ports[1], rear_port=rear_ports[1]), + ]) ModuleBay.objects.create(device=devices[0], name='Module Bay 1') ModuleBay.objects.create(device=devices[1], name='Module Bay 2') DeviceBay.objects.bulk_create(( @@ -5090,8 +5091,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil label='A', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, - rear_port=rear_ports[0], - rear_port_position=1, description='First', _site=devices[0].site, _location=devices[0].location, @@ -5104,8 +5103,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil label='B', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, - rear_port=rear_ports[1], - rear_port_position=2, description='Second', _site=devices[1].site, _location=devices[1].location, @@ -5118,8 +5115,6 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil label='C', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, - rear_port=rear_ports[2], - rear_port_position=3, description='Third', _site=devices[2].site, _location=devices[2].location, @@ -5130,8 +5125,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil name='Front Port 4', label='D', type=PortTypeChoices.TYPE_FC, - rear_port=rear_ports[3], - rear_port_position=1, + positions=2, _site=devices[3].site, _location=devices[3].location, _rack=devices[3].rack, @@ -5141,8 +5135,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil name='Front Port 5', label='E', type=PortTypeChoices.TYPE_FC, - rear_port=rear_ports[4], - rear_port_position=1, + positions=3, _site=devices[3].site, _location=devices[3].location, _rack=devices[3].rack, @@ -5152,14 +5145,21 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil name='Front Port 6', label='F', type=PortTypeChoices.TYPE_FC, - rear_port=rear_ports[5], - rear_port_position=1, + positions=4, _site=devices[3].site, _location=devices[3].location, _rack=devices[3].rack, ), ) FrontPort.objects.bulk_create(front_ports) + PortMapping.objects.bulk_create([ + PortMapping(device=devices[0], front_port=front_ports[0], rear_port=rear_ports[0]), + PortMapping(device=devices[1], front_port=front_ports[1], rear_port=rear_ports[1], rear_port_position=2), + PortMapping(device=devices[2], front_port=front_ports[2], rear_port=rear_ports[2], rear_port_position=3), + PortMapping(device=devices[3], front_port=front_ports[3], rear_port=rear_ports[3]), + PortMapping(device=devices[3], front_port=front_ports[4], rear_port=rear_ports[4]), + PortMapping(device=devices[3], front_port=front_ports[5], rear_port=rear_ports[5]), + ]) # Cables Cable(a_terminations=[front_ports[0]], b_terminations=[front_ports[3]]).save() @@ -5182,6 +5182,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_positions(self): + params = {'positions': [2, 3]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): params = {'description': ['First', 'Second']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -6420,13 +6424,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1') power_port = PowerPort.objects.create(device=devices[0], name='Power Port 1') power_outlet = PowerOutlet.objects.create(device=devices[0], name='Power Outlet 1') - rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1', positions=1) - front_port = FrontPort.objects.create( - device=devices[0], - name='Front Port 1', - rear_port=rear_port, - rear_port_position=1 - ) + rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1') + front_port = FrontPort.objects.create(device=devices[0], name='Front Port 1') + PortMapping.objects.create(device=devices[0], front_port=front_port, rear_port=rear_port) power_panel = PowerPanel.objects.create(name='Power Panel 1', site=sites[0]) power_feed = PowerFeed.objects.create(name='Power Feed 1', power_panel=power_panel) diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index fa654f789..a911cbf25 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -193,7 +193,8 @@ class FrontPortTestCase(TestCase): 'name': 'FrontPort[1-4]', 'label': 'Port[1-4]', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports], + 'positions': 1, + 'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports], } form = FrontPortCreateForm(front_port_data) @@ -208,7 +209,8 @@ class FrontPortTestCase(TestCase): 'name': 'FrontPort[1-4]', 'label': 'Port[1-2]', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports], + 'positions': 1, + 'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports], } form = FrontPortCreateForm(bad_front_port_data) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 877af600b..b3eff0920 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -444,13 +444,19 @@ class DeviceTestCase(TestCase): ) rearport.save() - FrontPortTemplate( + frontport = FrontPortTemplate( device_type=device_type, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, + ) + frontport.save() + + PortTemplateMapping.objects.create( + device_type=device_type, + front_port=frontport, rear_port=rearport, - rear_port_position=2 - ).save() + rear_port_position=2, + ) ModuleBayTemplate( device_type=device_type, @@ -528,11 +534,12 @@ class DeviceTestCase(TestCase): device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, - rear_port=rearport, - rear_port_position=2 + positions=1 ) self.assertEqual(frontport.cf['cf1'], 'foo') + self.assertTrue(PortMapping.objects.filter(front_port=frontport, rear_port=rearport).exists()) + modulebay = ModuleBay.objects.get( device=device, name='Module Bay 1' @@ -835,12 +842,18 @@ class CableTestCase(TestCase): ) RearPort.objects.bulk_create(rear_ports) front_ports = ( - FrontPort(device=patch_panel, name='FP1', type='8p8c', rear_port=rear_ports[0], rear_port_position=1), - FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1), - FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1), - FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1), + FrontPort(device=patch_panel, name='FP1', type='8p8c'), + FrontPort(device=patch_panel, name='FP2', type='8p8c'), + FrontPort(device=patch_panel, name='FP3', type='8p8c'), + FrontPort(device=patch_panel, name='FP4', type='8p8c'), ) FrontPort.objects.bulk_create(front_ports) + PortMapping.objects.bulk_create([ + PortMapping(device=patch_panel, front_port=front_ports[0], rear_port=rear_ports[0]), + PortMapping(device=patch_panel, front_port=front_ports[1], rear_port=rear_ports[1]), + PortMapping(device=patch_panel, front_port=front_ports[2], rear_port=rear_ports[2]), + PortMapping(device=patch_panel, front_port=front_ports[3], rear_port=rear_ports[3]), + ]) provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 6a34df652..52b02d982 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -741,17 +741,16 @@ class DeviceTypeTestCase( ) RearPortTemplate.objects.bulk_create(rear_ports) front_ports = ( - FrontPortTemplate( - device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1 - ), - FrontPortTemplate( - device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1 - ), - FrontPortTemplate( - device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1 - ), + FrontPortTemplate(device_type=devicetype, name='Front Port 1'), + FrontPortTemplate(device_type=devicetype, name='Front Port 2'), + FrontPortTemplate(device_type=devicetype, name='Front Port 3'), ) FrontPortTemplate.objects.bulk_create(front_ports) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]), + PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]), + PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]), + ]) url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) @@ -866,12 +865,16 @@ rear-ports: front-ports: - name: Front Port 1 type: 8p8c - rear_port: Rear Port 1 - name: Front Port 2 type: 8p8c - rear_port: Rear Port 2 - name: Front Port 3 type: 8p8c +port-mappings: + - front_port: Front Port 1 + rear_port: Rear Port 1 + - front_port: Front Port 2 + rear_port: Rear Port 2 + - front_port: Front Port 3 rear_port: Rear Port 3 module-bays: - name: Module Bay 1 @@ -971,8 +974,12 @@ inventory-items: self.assertEqual(device_type.frontporttemplates.count(), 3) fp1 = FrontPortTemplate.objects.first() self.assertEqual(fp1.name, 'Front Port 1') - self.assertEqual(fp1.rear_port, rp1) - self.assertEqual(fp1.rear_port_position, 1) + + self.assertEqual(device_type.port_mappings.count(), 3) + mapping1 = PortTemplateMapping.objects.first() + self.assertEqual(mapping1.device_type, device_type) + self.assertEqual(mapping1.front_port, fp1) + self.assertEqual(mapping1.rear_port, rp1) self.assertEqual(device_type.modulebaytemplates.count(), 3) mb1 = ModuleBayTemplate.objects.first() @@ -1316,17 +1323,16 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) RearPortTemplate.objects.bulk_create(rear_ports) front_ports = ( - FrontPortTemplate( - module_type=moduletype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1 - ), - FrontPortTemplate( - module_type=moduletype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1 - ), - FrontPortTemplate( - module_type=moduletype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1 - ), + FrontPortTemplate(module_type=moduletype, name='Front Port 1'), + FrontPortTemplate(module_type=moduletype, name='Front Port 2'), + FrontPortTemplate(module_type=moduletype, name='Front Port 3'), ) FrontPortTemplate.objects.bulk_create(front_ports) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping(module_type=moduletype, front_port=front_ports[0], rear_port=rear_ports[0]), + PortTemplateMapping(module_type=moduletype, front_port=front_ports[1], rear_port=rear_ports[1]), + PortTemplateMapping(module_type=moduletype, front_port=front_ports[2], rear_port=rear_ports[2]), + ]) url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) @@ -1394,12 +1400,16 @@ rear-ports: front-ports: - name: Front Port 1 type: 8p8c - rear_port: Rear Port 1 - name: Front Port 2 type: 8p8c - rear_port: Rear Port 2 - name: Front Port 3 type: 8p8c +port-mappings: + - front_port: Front Port 1 + rear_port: Rear Port 1 + - front_port: Front Port 2 + rear_port: Rear Port 2 + - front_port: Front Port 3 rear_port: Rear Port 3 module-bays: - name: Module Bay 1 @@ -1477,8 +1487,12 @@ module-bays: self.assertEqual(module_type.frontporttemplates.count(), 3) fp1 = FrontPortTemplate.objects.first() self.assertEqual(fp1.name, 'Front Port 1') - self.assertEqual(fp1.rear_port, rp1) - self.assertEqual(fp1.rear_port_position, 1) + + self.assertEqual(module_type.port_mappings.count(), 3) + mapping1 = PortTemplateMapping.objects.first() + self.assertEqual(mapping1.module_type, module_type) + self.assertEqual(mapping1.front_port, fp1) + self.assertEqual(mapping1.rear_port, rp1) self.assertEqual(module_type.modulebaytemplates.count(), 3) mb1 = ModuleBayTemplate.objects.first() @@ -1770,7 +1784,7 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') - rearports = ( + rear_ports = ( RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'), RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'), RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'), @@ -1778,35 +1792,33 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'), RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'), ) - RearPortTemplate.objects.bulk_create(rearports) - - FrontPortTemplate.objects.bulk_create( - ( - FrontPortTemplate( - device_type=devicetype, name='Front Port Template 1', rear_port=rearports[0], rear_port_position=1 - ), - FrontPortTemplate( - device_type=devicetype, name='Front Port Template 2', rear_port=rearports[1], rear_port_position=1 - ), - FrontPortTemplate( - device_type=devicetype, name='Front Port Template 3', rear_port=rearports[2], rear_port_position=1 - ), - ) + RearPortTemplate.objects.bulk_create(rear_ports) + front_ports = ( + FrontPortTemplate(device_type=devicetype, name='Front Port Template 1'), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 2'), + FrontPortTemplate(device_type=devicetype, name='Front Port Template 3'), ) + FrontPortTemplate.objects.bulk_create(front_ports) + PortTemplateMapping.objects.bulk_create([ + PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]), + PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]), + PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]), + ]) cls.form_data = { 'device_type': devicetype.pk, 'name': 'Front Port X', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rearports[3].pk, - 'rear_port_position': 1, + 'positions': 1, + 'rear_ports': [f'{rear_ports[3].pk}:1'], } cls.bulk_create_data = { 'device_type': devicetype.pk, 'name': 'Front Port [4-6]', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]], + 'positions': 1, + 'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]], } cls.bulk_edit_data = { @@ -2276,11 +2288,16 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) RearPort.objects.bulk_create(rear_ports) front_ports = ( - FrontPort(device=device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1), - FrontPort(device=device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1), - FrontPort(device=device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1), + FrontPort(device=device, name='Front Port Template 1'), + FrontPort(device=device, name='Front Port Template 2'), + FrontPort(device=device, name='Front Port Template 3'), ) FrontPort.objects.bulk_create(front_ports) + PortMapping.objects.bulk_create([ + PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]), + PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]), + PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]), + ]) url = reverse('dcim:device_frontports', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) @@ -3065,7 +3082,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): def setUpTestData(cls): device = create_test_device('Device 1') - rearports = ( + rear_ports = ( RearPort(device=device, name='Rear Port 1'), RearPort(device=device, name='Rear Port 2'), RearPort(device=device, name='Rear Port 3'), @@ -3073,14 +3090,19 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): RearPort(device=device, name='Rear Port 5'), RearPort(device=device, name='Rear Port 6'), ) - RearPort.objects.bulk_create(rearports) + RearPort.objects.bulk_create(rear_ports) front_ports = ( - FrontPort(device=device, name='Front Port 1', rear_port=rearports[0]), - FrontPort(device=device, name='Front Port 2', rear_port=rearports[1]), - FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]), + FrontPort(device=device, name='Front Port 1'), + FrontPort(device=device, name='Front Port 2'), + FrontPort(device=device, name='Front Port 3'), ) FrontPort.objects.bulk_create(front_ports) + PortMapping.objects.bulk_create([ + PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]), + PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]), + PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]), + ]) tags = create_tags('Alpha', 'Bravo', 'Charlie') @@ -3088,8 +3110,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): 'device': device.pk, 'name': 'Front Port X', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': rearports[3].pk, - 'rear_port_position': 1, + 'positions': 1, + 'rear_ports': [f'{rear_ports[3].pk}:1'], 'description': 'New description', 'tags': [t.pk for t in tags], } @@ -3098,7 +3120,8 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): 'device': device.pk, 'name': 'Front Port [4-6]', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]], + 'positions': 1, + 'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]], 'description': 'New description', 'tags': [t.pk for t in tags], } @@ -3109,10 +3132,10 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): } cls.csv_data = ( - "device,name,type,rear_port,rear_port_position", - "Device 1,Front Port 4,8p8c,Rear Port 4,1", - "Device 1,Front Port 5,8p8c,Rear Port 5,1", - "Device 1,Front Port 6,8p8c,Rear Port 6,1", + "device,name,type,positions", + "Device 1,Front Port 4,8p8c,1", + "Device 1,Front Port 5,8p8c,1", + "Device 1,Front Port 6,8p8c,1", ) cls.csv_update_data = ( diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 2380fbd0d..ce4a8c8d5 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -83,3 +83,36 @@ def update_interface_bridges(device, interface_templates, module=None): ) interface.full_clean() interface.save() + + +def create_port_mappings(device, device_type, module=None): + """ + Replicate all front/rear port mappings from a DeviceType to the given device. + """ + from dcim.models import FrontPort, PortMapping, RearPort + + templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port') + + # Cache front & rear ports for efficient lookups by name + front_ports = { + fp.name: fp for fp in FrontPort.objects.filter(device=device) + } + rear_ports = { + rp.name: rp for rp in RearPort.objects.filter(device=device) + } + + # Replicate PortMappings + mappings = [] + for template in templates: + front_port = front_ports.get(template.front_port.resolve_name(module=module)) + rear_port = rear_ports.get(template.rear_port.resolve_name(module=module)) + mappings.append( + PortMapping( + device_id=front_port.device_id, + front_port=front_port, + front_port_position=template.front_port_position, + rear_port=rear_port, + rear_port_position=template.rear_port_position, + ) + ) + PortMapping.objects.bulk_create(mappings) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 463d98179..b78685f5b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -42,6 +42,7 @@ from wireless.models import WirelessLAN from . import filtersets, forms, tables from .choices import DeviceFaceChoices, InterfaceModeChoices from .models import * +from .models.device_components import PortMapping from .object_actions import BulkAddComponents, BulkDisconnect CABLE_TERMINATION_TYPES = { @@ -1515,6 +1516,7 @@ class DeviceTypeImportView(generic.BulkImportView): 'interfaces': forms.InterfaceTemplateImportForm, 'rear-ports': forms.RearPortTemplateImportForm, 'front-ports': forms.FrontPortTemplateImportForm, + 'port-mappings': forms.PortTemplateMappingImportForm, 'module-bays': forms.ModuleBayTemplateImportForm, 'device-bays': forms.DeviceBayTemplateImportForm, 'inventory-items': forms.InventoryItemTemplateImportForm, @@ -1819,6 +1821,7 @@ class ModuleTypeImportView(generic.BulkImportView): 'interfaces': forms.InterfaceTemplateImportForm, 'rear-ports': forms.RearPortTemplateImportForm, 'front-ports': forms.FrontPortTemplateImportForm, + 'port-mappings': forms.PortTemplateMappingImportForm, 'module-bays': forms.ModuleBayTemplateImportForm, } @@ -3242,6 +3245,11 @@ class FrontPortListView(generic.ObjectListView): class FrontPortView(generic.ObjectView): queryset = FrontPort.objects.all() + def get_extra_context(self, request, instance): + return { + 'rear_port_mappings': PortMapping.objects.filter(front_port=instance).prefetch_related('rear_port'), + } + @register_model_view(FrontPort, 'add', detail=False) class FrontPortCreateView(generic.ComponentCreateView): @@ -3313,6 +3321,11 @@ class RearPortListView(generic.ObjectListView): class RearPortView(generic.ObjectView): queryset = RearPort.objects.all() + def get_extra_context(self, request, instance): + return { + 'front_port_mappings': PortMapping.objects.filter(rear_port=instance).prefetch_related('front_port'), + } + @register_model_view(RearPort, 'add', detail=False) class RearPortCreateView(generic.ComponentCreateView): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 88a3456f7..818cea610 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -1,6 +1,5 @@ import logging from collections import defaultdict -from copy import deepcopy from django.contrib import messages from django.db import router, transaction @@ -563,7 +562,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): if form.is_valid(): new_components = [] - data = deepcopy(request.POST) + data = request.POST.copy() pattern_count = len(form.cleaned_data[self.form.replication_fields[0]]) for i in range(pattern_count): @@ -572,7 +571,8 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): data[field_name] = form.cleaned_data[field_name][i] if hasattr(form, 'get_iterative_data'): - data.update(form.get_iterative_data(i)) + for k, v in form.get_iterative_data(i).items(): + data.setlist(k, v) component_form = self.model_form(data) diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html index 9f4d23e60..08a3a8d2f 100644 --- a/netbox/templates/dcim/frontport.html +++ b/netbox/templates/dcim/frontport.html @@ -47,12 +47,8 @@ - {% trans "Rear Port" %} - {{ object.rear_port|linkify }} - - - {% trans "Rear Port Position" %} - {{ object.rear_port_position }} + {% trans "Positions" %} + {{ object.positions }} {% trans "Description" %} @@ -65,6 +61,18 @@ {% plugin_left_page object %}
+
+

{% trans "Rear Ports" %}

+ + {% for mapping in rear_port_mappings %} + + + + + + {% endfor %} +
{{ mapping.front_port_position }}{{ mapping.rear_port|linkify }}{{ mapping.rear_port_position }}
+

{% trans "Connection" %}

{% if object.mark_connected %} diff --git a/netbox/templates/dcim/rearport.html b/netbox/templates/dcim/rearport.html index 3fed4307b..98cb70851 100644 --- a/netbox/templates/dcim/rearport.html +++ b/netbox/templates/dcim/rearport.html @@ -61,6 +61,18 @@ {% plugin_left_page object %}
+
+

{% trans "Rear Ports" %}

+ + {% for mapping in front_port_mappings %} + + + + + + {% endfor %} +
{{ mapping.rear_port_position }}{{ mapping.front_port|linkify }}{{ mapping.front_port_position }}
+

{% trans "Connection" %}

{% if object.mark_connected %} diff --git a/netbox/utilities/relations.py b/netbox/utilities/relations.py index d5e88299c..3514d394d 100644 --- a/netbox/utilities/relations.py +++ b/netbox/utilities/relations.py @@ -13,7 +13,7 @@ def get_related_models(model, ordered=True): related_models = [ (field.related_model, field.remote_field.name) for field in model._meta.related_objects - if type(field) is ManyToOneRel + if type(field) is ManyToOneRel and not getattr(field.related_model, '_netbox_private', False) ] if ordered: