diff --git a/docs/models/dcim/interfacetemplate.md b/docs/models/dcim/interfacetemplate.md index d9b30dd87..e11abcce4 100644 --- a/docs/models/dcim/interfacetemplate.md +++ b/docs/models/dcim/interfacetemplate.md @@ -1,3 +1,3 @@ ## Interface Templates -A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only." +A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only." Power over Ethernet (PoE) mode and type may also be assigned to interface templates. diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 211f264f0..68cff0547 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -97,7 +97,7 @@ Custom field UI visibility has no impact on API operation. * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location -### Bug Fixes +### Bug Fixes (from Beta1) * [#9728](https://github.com/netbox-community/netbox/issues/9728) - Fix validation when assigning a virtual machine to a device * [#9729](https://github.com/netbox-community/netbox/issues/9729) - Fix ordering of content type creation to ensure compatability with demo data @@ -177,6 +177,8 @@ Custom field UI visibility has no impact on API operation. * `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable` * Added the optional `poe_mode` and `poe_type` fields * Added the `l2vpn_termination` read-only field +* dcim.InterfaceTemplate + * Added the optional `poe_mode` and `poe_type` fields * dcim.Location * Added required `status` field (default value: `active`) * dcim.PowerOutlet diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5f30b7385..249a3f167 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -469,12 +469,22 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): default=None ) type = ChoiceField(choices=InterfaceTypeChoices) + poe_mode = ChoiceField( + choices=InterfacePoEModeChoices, + required=False, + allow_blank=True + ) + poe_type = ChoiceField( + choices=InterfacePoETypeChoices, + required=False, + allow_blank=True + ) class Meta: model = InterfaceTemplate fields = [ 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', - 'created', 'last_updated', + 'poe_mode', 'poe_type', 'created', 'last_updated', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 4bdc525a5..874d08ba5 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -652,6 +652,12 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo choices=InterfaceTypeChoices, null_value=None ) + poe_mode = django_filters.MultipleChoiceFilter( + choices=InterfacePoEModeChoices + ) + poe_type = django_filters.MultipleChoiceFilter( + choices=InterfacePoETypeChoices + ) class Meta: model = InterfaceTemplate diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 6d51302d3..8f765ae9b 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -818,8 +818,22 @@ class InterfaceTemplateBulkEditForm(BulkEditForm): description = forms.CharField( required=False ) + poe_mode = forms.ChoiceField( + choices=add_blank_choice(InterfacePoEModeChoices), + required=False, + initial='', + widget=StaticSelect(), + label='PoE mode' + ) + poe_type = forms.ChoiceField( + choices=add_blank_choice(InterfacePoETypeChoices), + required=False, + initial='', + widget=StaticSelect(), + label='PoE type' + ) - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description', 'poe_mode', 'poe_type') class FrontPortTemplateBulkEditForm(BulkEditForm): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 8d2e24c9c..c5474a2b1 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1027,11 +1027,13 @@ class InterfaceFilterForm(DeviceComponentFilterForm): ) poe_mode = MultipleChoiceField( choices=InterfacePoEModeChoices, - required=False + required=False, + label='PoE mode' ) poe_type = MultipleChoiceField( choices=InterfacePoEModeChoices, - required=False + required=False, + label='PoE type' ) rf_role = MultipleChoiceField( choices=WirelessRoleChoices, diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index aa573a4df..f3ab6f3a9 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1052,12 +1052,14 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', + 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type', ] widgets = { 'device_type': forms.HiddenInput(), 'module_type': forms.HiddenInput(), 'type': StaticSelect(), + 'poe_mode': StaticSelect(), + 'poe_type': StaticSelect(), } diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index afbcd6543..a51f48c5b 100644 --- a/netbox/dcim/forms/object_import.py +++ b/netbox/dcim/forms/object_import.py @@ -1,6 +1,6 @@ from django import forms -from dcim.choices import InterfaceTypeChoices, PortTypeChoices +from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.models import * from utilities.forms import BootstrapMixin @@ -112,11 +112,21 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): type = forms.ChoiceField( choices=InterfaceTypeChoices.CHOICES ) + poe_mode = forms.ChoiceField( + choices=InterfacePoEModeChoices, + required=False, + label='PoE mode' + ) + poe_type = forms.ChoiceField( + choices=InterfacePoETypeChoices, + required=False, + label='PoE type' + ) class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', + 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type', ] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index a43b293a4..52a98278a 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -258,6 +258,12 @@ class InterfaceTemplateType(ComponentTemplateObjectType): fields = '__all__' filterset_class = filtersets.InterfaceTemplateFilterSet + def resolve_poe_mode(self, info): + return self.poe_mode or None + + def resolve_poe_type(self, info): + return self.poe_type or None + class InventoryItemType(ComponentObjectType): diff --git a/netbox/dcim/migrations/0155_interface_poe_mode_type.py b/netbox/dcim/migrations/0155_interface_poe_mode_type.py index 0615d5d7e..13f2ddfc0 100644 --- a/netbox/dcim/migrations/0155_interface_poe_mode_type.py +++ b/netbox/dcim/migrations/0155_interface_poe_mode_type.py @@ -20,4 +20,14 @@ class Migration(migrations.Migration): name='poe_type', field=models.CharField(blank=True, max_length=50), ), + migrations.AddField( + model_name='interfacetemplate', + name='poe_mode', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interfacetemplate', + name='poe_type', + field=models.CharField(blank=True, max_length=50), + ), ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 92658d310..4a66bc457 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -1,6 +1,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from mptt.models import MPTTModel, TreeForeignKey @@ -318,6 +318,18 @@ class InterfaceTemplate(ModularComponentTemplateModel): default=False, verbose_name='Management only' ) + poe_mode = models.CharField( + max_length=50, + choices=InterfacePoEModeChoices, + blank=True, + verbose_name='PoE mode' + ) + poe_type = models.CharField( + max_length=50, + choices=InterfacePoETypeChoices, + blank=True, + verbose_name='PoE type' + ) component_model = Interface @@ -334,6 +346,8 @@ class InterfaceTemplate(ModularComponentTemplateModel): label=self.resolve_label(kwargs.get('module')), type=self.type, mgmt_only=self.mgmt_only, + poe_mode=self.poe_mode, + poe_type=self.poe_type, **kwargs ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 6075cb5a0..f8a28eb58 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -229,6 +229,8 @@ class DeviceType(NetBoxModel): 'mgmt_only': c.mgmt_only, 'label': c.label, 'description': c.description, + 'poe_mode': c.poe_mode, + 'poe_type': c.poe_type, } for c in self.interfacetemplates.all() ] diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 2da9daee7..3ed4d8c08 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -172,7 +172,7 @@ class InterfaceTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = InterfaceTemplate - fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions') + fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions') empty_text = "None" diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 21daa32c1..fbc0addb8 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1089,8 +1089,8 @@ 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, mgmt_only=True), - InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False), + InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, 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, 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), )) @@ -1113,6 +1113,14 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'mgmt_only': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_poe_mode(self): + params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_poe_type(self): + params = {'poe_type': [InterfacePoETypeChoices.TYPE_1_8023AF, InterfacePoETypeChoices.TYPE_2_8023AT]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FrontPortTemplate.objects.all()