diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index eaf4cbd18..f4a2af37a 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -475,7 +475,10 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): default=None ) type = ChoiceField(choices=InterfaceTypeChoices) - bridge = NestedInterfaceTemplateSerializer(required=False, allow_null=True) + bridge = NestedInterfaceTemplateSerializer( + required=False, + allow_null=True + ) poe_mode = ChoiceField( choices=InterfacePoEModeChoices, required=False, @@ -490,8 +493,8 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): class Meta: model = InterfaceTemplate fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'bridge', 'enabled', 'mgmt_only', 'description', - 'poe_mode', 'poe_type', 'created', 'last_updated', + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', + 'description', 'bridge', 'poe_mode', 'poe_type', 'created', 'last_updated', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 4b9ed583f..6f7ac419e 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -685,6 +685,10 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo choices=InterfaceTypeChoices, null_value=None ) + bridge_id = django_filters.ModelMultipleChoiceFilter( + field_name='bridge', + queryset=InterfaceTemplate.objects.all() + ) poe_mode = django_filters.MultipleChoiceFilter( choices=InterfacePoEModeChoices ) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index e2d1cb50d..3da081c06 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -377,6 +377,8 @@ class InterfaceTemplate(ModularComponentTemplateModel): super().clean() if self.bridge: + if self.pk and self.bridge_id == self.pk: + raise ValidationError({'bridge': "An interface cannot be bridged to itself."}) if self.device_type and self.device_type != self.bridge.device_type: raise ValidationError({ 'bridge': f"Bridge interface ({self.bridge}) must belong to the same device type" diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 01ef4a87b..4b82e87bd 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1142,11 +1142,36 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): ) DeviceType.objects.bulk_create(device_types) - InterfaceTemplate.objects.bulk_create(( - InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=True, mgmt_only=True, poe_mode=InterfacePoEModeChoices.MODE_PD, poe_type=InterfacePoETypeChoices.TYPE_1_8023AF), - InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=False, mgmt_only=False, poe_mode=InterfacePoEModeChoices.MODE_PSE, poe_type=InterfacePoETypeChoices.TYPE_2_8023AT), - InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False), - )) + interface_templates = ( + InterfaceTemplate( + device_type=device_types[0], + name='Interface 1', + type=InterfaceTypeChoices.TYPE_1GE_FIXED, + enabled=True, + mgmt_only=True, + poe_mode=InterfacePoEModeChoices.MODE_PD, + poe_type=InterfacePoETypeChoices.TYPE_1_8023AF + ), + InterfaceTemplate( + device_type=device_types[1], + name='Interface 2', + type=InterfaceTypeChoices.TYPE_1GE_GBIC, + enabled=False, + mgmt_only=False, + poe_mode=InterfacePoEModeChoices.MODE_PSE, + poe_type=InterfacePoETypeChoices.TYPE_2_8023AT + ), + InterfaceTemplate( + device_type=device_types[2], + name='Interface 3', + type=InterfaceTypeChoices.TYPE_1GE_SFP, + mgmt_only=False + ), + ) + InterfaceTemplate.objects.bulk_create(interface_templates) + interface_templates[0].bridge = interface_templates[1] + interface_templates[1].bridge = interface_templates[0] + InterfaceTemplate.objects.bulk_update(interface_templates, ['bridge']) def test_name(self): params = {'name': ['Interface 1', 'Interface 2']} @@ -1173,6 +1198,10 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'mgmt_only': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_bridge(self): + params = {'bridge_id': [InterfaceTemplate.objects.filter(bridge__isnull=False).first().bridge_id]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_poe_mode(self): params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)