From a024e1c6ee383ac9de508c7852c5b425aa521410 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Tue, 27 Jan 2026 19:03:15 +0100 Subject: [PATCH] fix(dcim): Add port mapping creation for module install Implements automatic creation of port mappings when a module with front/rear port templates is installed. Ensures mappings are replicated accurately from the module type and verifies their correctness with new test cases. Fixes #21269 --- netbox/dcim/models/modules.py | 4 +- netbox/dcim/tests/test_models.py | 136 +++++++++++++++++++++++++++++++ netbox/dcim/utils.py | 6 +- 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 8330d9f26..d718cc08c 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from jsonschema.exceptions import ValidationError as JSONValidationError from dcim.choices import * -from dcim.utils import update_interface_bridges +from dcim.utils import create_port_mappings, update_interface_bridges from extras.models import ConfigContextModel, CustomField from netbox.models import PrimaryModel from netbox.models.features import ImageAttachmentsMixin @@ -361,5 +361,7 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel): update_fields=update_fields ) + # Replicate any front/rear port mappings from the ModuleType + create_port_mappings(self.device, self.module_type, self) # Interface bridges have to be set after interface instantiation update_interface_bridges(self.device, self.module_type.interfacetemplates, self) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index c4b9e37bb..6e843015e 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -875,6 +875,142 @@ class ModuleBayTestCase(TestCase): self.assertIsNone(bay2.parent) self.assertIsNone(bay2.module) + def test_module_installation_creates_port_mappings(self): + """ + Test that installing a module with front/rear port templates correctly + creates PortMapping instances for the device. + """ + device = Device.objects.first() + manufacturer = Manufacturer.objects.first() + module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 1') + + # Create a module type with a rear port template + module_type_with_mappings = ModuleType.objects.create( + manufacturer=manufacturer, + model='Module Type With Mappings', + ) + + # Create a rear port template with 12 positions (splice) + rear_port_template = RearPortTemplate.objects.create( + module_type=module_type_with_mappings, + name='Rear Port 1', + type=PortTypeChoices.TYPE_SPLICE, + positions=12, + ) + + # Create 12 front port templates mapped to the rear port + front_port_templates = [] + for i in range(1, 13): + front_port_template = FrontPortTemplate.objects.create( + module_type=module_type_with_mappings, + name=f'port {i}', + type=PortTypeChoices.TYPE_LC, + positions=1, + ) + front_port_templates.append(front_port_template) + + # Create port template mapping + PortTemplateMapping.objects.create( + device_type=None, + module_type=module_type_with_mappings, + front_port=front_port_template, + front_port_position=1, + rear_port=rear_port_template, + rear_port_position=i, + ) + + # Install the module + module = Module.objects.create( + device=device, + module_bay=module_bay, + module_type=module_type_with_mappings, + status=ModuleStatusChoices.STATUS_ACTIVE, + ) + + # Verify that front ports were created + front_ports = FrontPort.objects.filter(device=device, module=module) + self.assertEqual(front_ports.count(), 12) + + # Verify that the rear port was created + rear_ports = RearPort.objects.filter(device=device, module=module) + self.assertEqual(rear_ports.count(), 1) + rear_port = rear_ports.first() + self.assertEqual(rear_port.positions, 12) + + # Verify that port mappings were created + port_mappings = PortMapping.objects.filter(front_port__module=module) + self.assertEqual(port_mappings.count(), 12) + + # Verify each mapping is correct + for i, front_port_template in enumerate(front_port_templates, start=1): + front_port = FrontPort.objects.get( + device=device, + name=front_port_template.name, + module=module, + ) + + # Check that a mapping exists for this front port + mapping = PortMapping.objects.get( + device=device, + front_port=front_port, + front_port_position=1, + ) + + self.assertEqual(mapping.rear_port, rear_port) + self.assertEqual(mapping.front_port_position, 1) + self.assertEqual(mapping.rear_port_position, i) + + def test_module_installation_without_mappings(self): + """ + Test that installing a module without port template mappings + doesn't create any PortMapping instances. + """ + device = Device.objects.first() + manufacturer = Manufacturer.objects.first() + module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 2') + + # Create a module type without any port template mappings + module_type_no_mappings = ModuleType.objects.create( + manufacturer=manufacturer, + model='Module Type Without Mappings', + ) + + # Create a rear port template + RearPortTemplate.objects.create( + module_type=module_type_no_mappings, + name='Rear Port 1', + type=PortTypeChoices.TYPE_SPLICE, + positions=12, + ) + + # Create front port templates but DO NOT create PortTemplateMapping rows + for i in range(1, 13): + FrontPortTemplate.objects.create( + module_type=module_type_no_mappings, + name=f'port {i}', + type=PortTypeChoices.TYPE_LC, + positions=1, + ) + + # Install the module + module = Module.objects.create( + device=device, + module_bay=module_bay, + module_type=module_type_no_mappings, + status=ModuleStatusChoices.STATUS_ACTIVE, + ) + + # Verify no port mappings were created for this module + port_mappings = PortMapping.objects.filter( + device=device, + front_port__module=module, + front_port_position=1, + ) + self.assertEqual(port_mappings.count(), 0) + self.assertEqual(FrontPort.objects.filter(module=module).count(), 12) + self.assertEqual(RearPort.objects.filter(module=module).count(), 1) + self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0) + class CableTestCase(TestCase): diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 4b9d0fb5c..5dcc20035 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -85,13 +85,13 @@ def update_interface_bridges(device, interface_templates, module=None): interface.save() -def create_port_mappings(device, device_type, module=None): +def create_port_mappings(device, device_or_module_type, module=None): """ - Replicate all front/rear port mappings from a DeviceType to the given device. + Replicate all front/rear port mappings from a DeviceType or ModuleType to the given device. """ from dcim.models import FrontPort, PortMapping, RearPort - templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port') + templates = device_or_module_type.port_mappings.prefetch_related('front_port', 'rear_port') # Cache front & rear ports for efficient lookups by name front_ports = {