diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 04929b079..a611f64d0 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -514,12 +514,18 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): allow_blank=True, allow_null=True ) + rf_role = ChoiceField( + choices=WirelessRoleChoices, + required=False, + allow_blank=True, + allow_null=True + ) class Meta: model = InterfaceTemplate fields = [ 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', - 'description', 'bridge', 'poe_mode', 'poe_type', 'created', 'last_updated', + 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index e88fc120d..416c022ce 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -696,6 +696,9 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo poe_type = django_filters.MultipleChoiceFilter( choices=InterfacePoETypeChoices ) + rf_role = django_filters.MultipleChoiceFilter( + choices=WirelessRoleChoices + ) class Meta: model = InterfaceTemplate diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 179ff9b67..2d86a1718 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -76,14 +76,14 @@ class PowerOutletBulkCreateForm( class InterfaceBulkCreateForm( form_from_model(Interface, [ - 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', + 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', 'rf_role' ]), DeviceBulkAddComponentForm ): model = Interface field_order = ( 'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode', - 'poe_type', 'mark_connected', 'description', 'tags', + 'poe_type', 'mark_connected', 'rf_role', 'description', 'tags', ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 93b769738..5a465bfc8 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -15,6 +15,7 @@ from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from wireless.models import WirelessLAN, WirelessLANGroup +from wireless.choices import WirelessRoleChoices __all__ = ( 'CableBulkEditForm', @@ -922,8 +923,14 @@ class InterfaceTemplateBulkEditForm(BulkEditForm): initial='', label=_('PoE type') ) + rf_role = forms.ChoiceField( + choices=add_blank_choice(WirelessRoleChoices), + required=False, + initial='', + label=_('Wireless role') + ) - nullable_fields = ('label', 'description', 'poe_mode', 'poe_type') + nullable_fields = ('label', 'description', 'poe_mode', 'poe_type', 'rf_role') class FrontPortTemplateBulkEditForm(BulkEditForm): diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 067cf2bda..632dabb81 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -826,13 +826,14 @@ class InterfaceTemplateForm(ModularComponentTemplateForm): fieldsets = ( (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')), - ('PoE', ('poe_mode', 'poe_type')) + ('PoE', ('poe_mode', 'poe_type')), + ('Wireless', ('rf_role',)) ) class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge', + 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge', 'rf_role', ] diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index 9328a3f72..01efbe123 100644 --- a/netbox/dcim/forms/object_import.py +++ b/netbox/dcim/forms/object_import.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext as _ from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.models import * from utilities.forms import BootstrapMixin +from wireless.choices import WirelessRoleChoices __all__ = ( 'ConsolePortTemplateImportForm', @@ -96,11 +97,17 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm): required=False, label=_('PoE type') ) + rf_role = forms.ChoiceField( + choices=WirelessRoleChoices, + required=False, + label=_('Wireless role') + ) class Meta: model = InterfaceTemplate fields = [ - 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'poe_mode', 'poe_type', + 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'poe_mode', + 'poe_type', 'rf_role' ] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 3c6c0a885..7d7434587 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -277,6 +277,9 @@ class InterfaceTemplateType(ComponentTemplateObjectType): def resolve_poe_type(self, info): return self.poe_type or None + def resolve_rf_role(self, info): + return self.rf_role or None + class InventoryItemType(ComponentObjectType): component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType') diff --git a/netbox/dcim/migrations/0179_interfacetemplate_rf_role.py b/netbox/dcim/migrations/0179_interfacetemplate_rf_role.py new file mode 100644 index 000000000..44eb08853 --- /dev/null +++ b/netbox/dcim/migrations/0179_interfacetemplate_rf_role.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-07-18 07:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0178_virtual_chassis_member_counter'), + ] + + operations = [ + migrations.AddField( + model_name='interfacetemplate', + name='rf_role', + field=models.CharField(blank=True, max_length=30), + ), + ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 7d669bca0..d4539a6ab 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -13,6 +13,7 @@ from utilities.fields import ColorField, NaturalOrderingField from utilities.mptt import TreeManager from utilities.ordering import naturalize_interface from utilities.tracking import TrackingModelMixin +from wireless.choices import WirelessRoleChoices from .device_components import ( ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, RearPort, @@ -388,6 +389,12 @@ class InterfaceTemplate(ModularComponentTemplateModel): blank=True, verbose_name='PoE type' ) + rf_role = models.CharField( + max_length=30, + choices=WirelessRoleChoices, + blank=True, + verbose_name='Wireless role' + ) component_model = Interface @@ -406,6 +413,11 @@ class InterfaceTemplate(ModularComponentTemplateModel): 'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type" }) + if self.rf_role and self.type not in WIRELESS_IFACE_TYPES: + raise ValidationError({ + 'rf_role': "Wireless role may be set only on wireless interfaces." + }) + def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -415,6 +427,7 @@ class InterfaceTemplate(ModularComponentTemplateModel): mgmt_only=self.mgmt_only, poe_mode=self.poe_mode, poe_type=self.poe_type, + rf_role=self.rf_role, **kwargs ) instantiate.do_not_call_in_templates = True @@ -430,6 +443,7 @@ class InterfaceTemplate(ModularComponentTemplateModel): 'bridge': self.bridge.name if self.bridge else None, 'poe_mode': self.poe_mode, 'poe_type': self.poe_type, + 'rf_role': self.rf_role, } diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 65d0c1707..d24ed2f13 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -219,7 +219,10 @@ class InterfaceTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = models.InterfaceTemplate - fields = ('pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type', 'actions') + fields = ( + 'pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type', + 'rf_role', 'actions', + ) empty_text = "None"