Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0901694
Initial work on FR #20788 (cable profiles)
jeremystretch Nov 13, 2025
a20ac40
Add missing filters for cable_position
jeremystretch Nov 14, 2025
481811e
Misc cleanup
jeremystretch Nov 14, 2025
7edea73
Add initial cable path tests for profiles
jeremystretch Nov 14, 2025
fe95d89
Fix test
jeremystretch Nov 14, 2025
2fe5323
Add topology tests for cable profiles
jeremystretch Nov 14, 2025
fb2ea37
Simplify A/B side popping logic
jeremystretch Nov 14, 2025
24c6653
Add profile for 4x4 MPO8 shuffle cable
jeremystretch Nov 14, 2025
a75dee7
Enable drag-and-drop of items within multiselect fields
jeremystretch Nov 14, 2025
aa7eeda
Remove many-to-one profiles
jeremystretch Nov 17, 2025
576c0db
Document profile field
jeremystretch Nov 17, 2025
5a7f86a
Clean up cable profiles
jeremystretch Nov 17, 2025
b235c5c
Rebase migrations
jeremystretch Nov 17, 2025
2b420bd
Introduce PortAssignment M2M mapping
jeremystretch Nov 17, 2025
c09b077
Add positions field on FrontPort; remove legacy fields
jeremystretch Nov 17, 2025
6a7027a
Update FrontPort model form
jeremystretch Nov 18, 2025
4790dbb
Exclude occupied rear port & position pairs from list of choices
jeremystretch Nov 18, 2025
a7c3971
Fix FrontPortCreateForm
jeremystretch Nov 18, 2025
5e8d57f
Update path tracing logic (WIP)
jeremystretch Nov 18, 2025
f49b88a
Permit FrontPort.positions to be null
jeremystretch Nov 19, 2025
9dbb9bb
Update cable path tests
jeremystretch Nov 19, 2025
4f54b29
Default FrontPort.positions to 1, to match RearPort
jeremystretch Nov 20, 2025
f067122
Add PortAssignmentTemplate for device types
jeremystretch Nov 20, 2025
1e0748e
Refactor PortAssignment and PortAssignmentTemplate into PortAssignmen…
jeremystretch Nov 20, 2025
e71e4ef
Replicate front/rear port assignments from DeviceType
jeremystretch Nov 20, 2025
b9d57c7
Update migrations
jeremystretch Nov 21, 2025
5b8d80a
Fix filterset tests
jeremystretch Nov 21, 2025
bfff2d7
Update API tests
jeremystretch Nov 21, 2025
66bbfa7
Remove rear_ports M2M fields from FrontPort & FrontPortTemplate
jeremystretch Nov 21, 2025
85d4066
Simplify nested port assignment representation
jeremystretch Nov 21, 2025
6262010
UI cleanup for front/rear ports
jeremystretch Nov 21, 2025
b538ff8
Clean up tests
jeremystretch Nov 21, 2025
d2afab9
Remove obsolete GraphQL filters
jeremystretch Nov 21, 2025
2e37e49
Merge branch 'feature' into 20564-port-mappings
jeremystretch Nov 25, 2025
8185838
Merge branch 'feature' into 20564-port-mappings
jeremystretch Nov 25, 2025
b993ec9
Rename port assignments to port mappings
jeremystretch Nov 26, 2025
7c21936
Consolidate create() and update() logic into PortSerializer base class
jeremystretch Nov 26, 2025
ca88021
Validate position count on FrontPort & FrontPortTemplate
jeremystretch Nov 26, 2025
06447ac
Optimize replication of port mappings from DeviceType
jeremystretch Nov 26, 2025
e6b1f94
Misc cleanup
jeremystretch Nov 26, 2025
006407f
get_related_models() should ignore models marked as private
jeremystretch Nov 26, 2025
836ddbf
Add FKs from PortMapping & PortTemplateMapping to their parent models
jeremystretch Nov 26, 2025
715974c
Update GraphQL types & filters
jeremystretch Nov 26, 2025
9e367b8
Update logic for handling split cable paths
jeremystretch Nov 26, 2025
fa70430
Enable defining port mappings when importing device/module types
jeremystretch Nov 26, 2025
665f91f
Extend REST API tests to check for updated port mappings after modify…
jeremystretch Dec 1, 2025
5597664
Cleaned up debugging logs in CablePath
jeremystretch Dec 1, 2025
463f37a
Refactor form validation
jeremystretch Dec 1, 2025
a3909d5
Allow for null cable_position
jeremystretch Dec 1, 2025
d5ce6d2
Add tests for new positions filters
jeremystretch Dec 1, 2025
9198a04
Remove obsolete form validation logic
jeremystretch Dec 1, 2025
619728a
Ensure cable paths are retraced when port mappings are changes via form
jeremystretch Dec 1, 2025
107c1f2
Add tests for PortMapping changes
jeremystretch Dec 4, 2025
80d177f
Merge branch 'feature' into 20564-port-mappings
jeremystretch Dec 9, 2025
ba5c854
Clean up presentation of port mappings under front/rear port detail v…
jeremystretch Dec 9, 2025
a09c9a1
Rearranged panels under front/rear port views
jeremystretch Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions netbox/dcim/api/serializers_/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
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',
)


Expand Down Expand Up @@ -35,3 +37,53 @@ def get_connected_endpoints(self, obj):
@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
61 changes: 43 additions & 18 deletions netbox/dcim/api/serializers_/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
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
Expand Down Expand Up @@ -294,7 +294,20 @@ def validate(self, data):
return super().validate(data)


class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class RearPortMappingSerializer(serializers.ModelSerializer):
position = serializers.IntegerField(
source='rear_port_position'
)
front_port = serializers.PrimaryKeyRelatedField(
queryset=FrontPort.objects.all(),
)

class Meta:
model = PortMapping
fields = ('position', 'front_port', 'front_port_position')


class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
Expand All @@ -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,
Expand All @@ -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')

Expand Down
53 changes: 45 additions & 8 deletions netbox/dcim/api/serializers_/devicetype_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
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
Expand Down Expand Up @@ -205,7 +207,20 @@ class Meta:
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,
Expand All @@ -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,
Expand All @@ -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')

Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
# RearPorts
#

REARPORT_POSITIONS_MIN = 1
REARPORT_POSITIONS_MAX = 1024
PORT_POSITION_MIN = 1
PORT_POSITION_MAX = 1024


#
Expand Down
26 changes: 22 additions & 4 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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',
)

Expand All @@ -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
Expand Down
Loading
Loading