diff --git a/docs/models/wireless/wirelesslan.md b/docs/models/wireless/wirelesslan.md index 5bb3dbd65..0f50fa75f 100644 --- a/docs/models/wireless/wirelesslan.md +++ b/docs/models/wireless/wirelesslan.md @@ -12,6 +12,13 @@ The service set identifier (SSID) for the wireless network. The [wireless LAN group](./wirelesslangroup.md) to which this wireless LAN is assigned (if any). +### Status + +The operational status of the wireless network. + +!!! tip + Additional statuses may be defined by setting `WirelessLAN.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. + ### VLAN Each wireless LAN can optionally be mapped to a [VLAN](../ipam/vlan.md), to model a bridge between wired and wireless segments. diff --git a/netbox/templates/wireless/wirelesslan.html b/netbox/templates/wireless/wirelesslan.html index 19e8b930d..ad76f9c07 100644 --- a/netbox/templates/wireless/wirelesslan.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -18,6 +18,10 @@ Group {{ object.group|linkify|placeholder }} + + Status + {% badge object.get_status_display bg_color=object.get_status_color %} + Description {{ object.description|placeholder }} diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 109c3a341..cc2c8701c 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -33,6 +33,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer): class WirelessLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') group = NestedWirelessLANGroupSerializer(required=False, allow_null=True) + status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True) vlan = NestedVLANSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) @@ -41,8 +42,8 @@ class WirelessLANSerializer(NetBoxModelSerializer): class Meta: model = WirelessLAN fields = [ - 'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'tenant', 'auth_type', 'auth_cipher', - 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type', + 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/wireless/choices.py b/netbox/wireless/choices.py index 135fa1b0c..b1f283620 100644 --- a/netbox/wireless/choices.py +++ b/netbox/wireless/choices.py @@ -1,3 +1,5 @@ +from django.utils.translation import gettext as _ + from utilities.choices import ChoiceSet @@ -11,6 +13,22 @@ class WirelessRoleChoices(ChoiceSet): ) +class WirelessLANStatusChoices(ChoiceSet): + key = 'WirelessLANS.status' + + STATUS_ACTIVE = 'active' + STATUS_RESERVED = 'reserved' + STATUS_DISABLED = 'disabled' + STATUS_DEPRECATED = 'deprecated' + + CHOICES = [ + (STATUS_ACTIVE, _('Active'), 'green'), + (STATUS_RESERVED, _('Reserved'), 'cyan'), + (STATUS_DISABLED, _('Disabled'), 'orange'), + (STATUS_DEPRECATED, _('Deprecated'), 'red'), + ] + + class WirelessChannelChoices(ChoiceSet): # 2.4 GHz diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 60c4f935b..6ffb9cb91 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -43,6 +43,9 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): lookup_expr='in', to_field_name='slug' ) + status = django_filters.MultipleChoiceFilter( + choices=WirelessLANStatusChoices + ) vlan_id = django_filters.ModelMultipleChoiceFilter( queryset=VLAN.objects.all() ) diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 543e7e0b3..7544327a5 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -34,6 +34,10 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): + status = forms.ChoiceField( + choices=add_blank_choice(WirelessLANStatusChoices), + required=False + ) group = DynamicModelChoiceField( queryset=WirelessLANGroup.objects.all(), required=False @@ -75,7 +79,7 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLAN fieldsets = ( - (None, ('group', 'ssid', 'vlan', 'tenant', 'description')), + (None, ('group', 'ssid', 'status', 'vlan', 'tenant', 'description')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) nullable_fields = ( diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 00078c8eb..4d96f60ad 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -35,6 +35,10 @@ class WirelessLANCSVForm(NetBoxModelCSVForm): to_field_name='name', help_text='Assigned group' ) + status = CSVChoiceField( + choices=WirelessLANStatusChoices, + help_text='Operational status' + ) vlan = CSVModelChoiceField( queryset=VLAN.objects.all(), required=False, @@ -61,8 +65,8 @@ class WirelessLANCSVForm(NetBoxModelCSVForm): class Meta: model = WirelessLAN fields = ( - 'ssid', 'group', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', - 'tags', + 'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', + 'comments', 'tags', ) diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index d7a6aac6e..c3e63687d 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -29,7 +29,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLAN fieldsets = ( (None, ('q', 'filter', 'tag')), - ('Attributes', ('ssid', 'group_id',)), + ('Attributes', ('ssid', 'group_id', 'status')), ('Tenant', ('tenant_group_id', 'tenant_id')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) @@ -43,6 +43,11 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): null_option='None', label=_('Group') ) + status = forms.ChoiceField( + required=False, + choices=add_blank_choice(WirelessLANStatusChoices), + widget=StaticSelect() + ) auth_type = forms.ChoiceField( required=False, choices=add_blank_choice(WirelessAuthTypeChoices), diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index d57c74575..e59c36696 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -37,7 +37,6 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): queryset=WirelessLANGroup.objects.all(), required=False ) - region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -85,7 +84,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - ('Wireless LAN', ('ssid', 'group', 'description', 'tags')), + ('Wireless LAN', ('ssid', 'group', 'status', 'description', 'tags')), ('VLAN', ('region', 'site_group', 'site', 'vlan_group', 'vlan',)), ('Tenancy', ('tenant_group', 'tenant')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), @@ -94,10 +93,11 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): class Meta: model = WirelessLAN fields = [ - 'ssid', 'group', 'region', 'site_group', 'site', 'vlan_group', 'vlan', 'tenant_group', 'tenant', + 'ssid', 'group', 'region', 'site_group', 'site', 'status', 'vlan_group', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', ] widgets = { + 'status': StaticSelect, 'auth_type': StaticSelect, 'auth_cipher': StaticSelect, } diff --git a/netbox/wireless/migrations/0008_wirelesslan_status.py b/netbox/wireless/migrations/0008_wirelesslan_status.py new file mode 100644 index 000000000..e7832aba2 --- /dev/null +++ b/netbox/wireless/migrations/0008_wirelesslan_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.2 on 2022-11-04 17:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wireless', '0007_standardize_description_comments'), + ] + + operations = [ + migrations.AddField( + model_name='wirelesslan', + name='status', + field=models.CharField(default='active', max_length=50), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 96764b53c..5858e641c 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -84,6 +84,11 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel): blank=True, null=True ) + status = models.CharField( + max_length=50, + choices=WirelessLANStatusChoices, + default=WirelessLANStatusChoices.STATUS_ACTIVE + ) vlan = models.ForeignKey( to='ipam.VLAN', on_delete=models.PROTECT, @@ -111,6 +116,9 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel): def get_absolute_url(self): return reverse('wireless:wirelesslan', args=[self.pk]) + def get_status_color(self): + return WirelessLANStatusChoices.colors.get(self.status) + def get_wireless_interface_types(): # Wrap choices in a callable to avoid generating dummy migrations diff --git a/netbox/wireless/tables/wirelesslan.py b/netbox/wireless/tables/wirelesslan.py index 4aa5cc1fd..5d17465f0 100644 --- a/netbox/wireless/tables/wirelesslan.py +++ b/netbox/wireless/tables/wirelesslan.py @@ -42,6 +42,7 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable): group = tables.Column( linkify=True ) + status = columns.ChoiceFieldColumn() interface_count = tables.Column( verbose_name='Interfaces' ) @@ -53,10 +54,10 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = WirelessLAN fields = ( - 'pk', 'ssid', 'group', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type', 'auth_cipher', - 'auth_psk', 'description', 'comments', 'tags', 'created', 'last_updated', + 'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type', + 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'created', 'last_updated', ) - default_columns = ('pk', 'ssid', 'group', 'description', 'vlan', 'auth_type', 'interface_count') + default_columns = ('pk', 'ssid', 'group', 'status', 'description', 'vlan', 'auth_type', 'interface_count') class WirelessLANInterfacesTable(NetBoxTable): diff --git a/netbox/wireless/tests/test_api.py b/netbox/wireless/tests/test_api.py index 9ef552eb7..cfc17c660 100644 --- a/netbox/wireless/tests/test_api.py +++ b/netbox/wireless/tests/test_api.py @@ -68,9 +68,9 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase): group.save() wireless_lans = ( - WirelessLAN(ssid='WLAN1'), - WirelessLAN(ssid='WLAN2'), - WirelessLAN(ssid='WLAN3'), + WirelessLAN(ssid='WLAN1', status=WirelessLANStatusChoices.STATUS_ACTIVE), + WirelessLAN(ssid='WLAN2', status=WirelessLANStatusChoices.STATUS_ACTIVE), + WirelessLAN(ssid='WLAN3', status=WirelessLANStatusChoices.STATUS_ACTIVE), ) WirelessLAN.objects.bulk_create(wireless_lans) @@ -78,23 +78,27 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase): { 'ssid': 'WLAN4', 'group': groups[0].pk, + 'status': WirelessLANStatusChoices.STATUS_DISABLED, 'tenant': tenants[0].pk, 'auth_type': WirelessAuthTypeChoices.TYPE_OPEN, }, { 'ssid': 'WLAN5', 'group': groups[1].pk, + 'status': WirelessLANStatusChoices.STATUS_DISABLED, 'tenant': tenants[0].pk, 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, }, { 'ssid': 'WLAN6', + 'status': WirelessLANStatusChoices.STATUS_DISABLED, 'tenant': tenants[0].pk, 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE, }, ] cls.bulk_update_data = { + 'status': WirelessLANStatusChoices.STATUS_DEPRECATED, 'group': groups[2].pk, 'tenant': tenants[1].pk, 'description': 'New description', diff --git a/netbox/wireless/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py index ffe919c32..0629fea07 100644 --- a/netbox/wireless/tests/test_filtersets.py +++ b/netbox/wireless/tests/test_filtersets.py @@ -64,9 +64,18 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): def setUpTestData(cls): groups = ( - WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'), - WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'), - WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'), + WirelessLANGroup( + name='Wireless LAN Group 1', + slug='wireless-lan-group-1' + ), + WirelessLANGroup( + name='Wireless LAN Group 2', + slug='wireless-lan-group-2' + ), + WirelessLANGroup( + name='Wireless LAN Group 3', + slug='wireless-lan-group-3' + ), ) for group in groups: group.save() @@ -86,9 +95,36 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): Tenant.objects.bulk_create(tenants) wireless_lans = ( - WirelessLAN(ssid='WLAN1', group=groups[0], vlan=vlans[0], tenant=tenants[0], auth_type=WirelessAuthTypeChoices.TYPE_OPEN, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_psk='PSK1'), - WirelessLAN(ssid='WLAN2', group=groups[1], vlan=vlans[1], tenant=tenants[1], auth_type=WirelessAuthTypeChoices.TYPE_WEP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_psk='PSK2'), - WirelessLAN(ssid='WLAN3', group=groups[2], vlan=vlans[2], tenant=tenants[2], auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, auth_psk='PSK3'), + WirelessLAN( + ssid='WLAN1', + group=groups[0], + status=WirelessLANStatusChoices.STATUS_ACTIVE, + vlan=vlans[0], + tenant=tenants[0], + auth_type=WirelessAuthTypeChoices.TYPE_OPEN, + auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, + auth_psk='PSK1' + ), + WirelessLAN( + ssid='WLAN2', + group=groups[1], + status=WirelessLANStatusChoices.STATUS_DISABLED, + vlan=vlans[1], + tenant=tenants[1], + auth_type=WirelessAuthTypeChoices.TYPE_WEP, + auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, + auth_psk='PSK2' + ), + WirelessLAN( + ssid='WLAN3', + group=groups[2], + status=WirelessLANStatusChoices.STATUS_RESERVED, + vlan=vlans[2], + tenant=tenants[2], + auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, + auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, + auth_psk='PSK3' + ), ) WirelessLAN.objects.bulk_create(wireless_lans) @@ -103,6 +139,10 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'group': [groups[0].slug, groups[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_status(self): + params = {'status': [WirelessLANStatusChoices.STATUS_ACTIVE, WirelessLANStatusChoices.STATUS_DISABLED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_vlan(self): vlans = VLAN.objects.all()[:2] params = {'vlan_id': [vlans[0].pk, vlans[1].pk]} diff --git a/netbox/wireless/tests/test_views.py b/netbox/wireless/tests/test_views.py index 615678a62..62c3b451f 100644 --- a/netbox/wireless/tests/test_views.py +++ b/netbox/wireless/tests/test_views.py @@ -70,9 +70,24 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): group.save() wireless_lans = ( - WirelessLAN(group=groups[0], ssid='WLAN1', tenant=tenants[0]), - WirelessLAN(group=groups[0], ssid='WLAN2', tenant=tenants[0]), - WirelessLAN(group=groups[0], ssid='WLAN3', tenant=tenants[0]), + WirelessLAN( + group=groups[0], + ssid='WLAN1', + status=WirelessLANStatusChoices.STATUS_ACTIVE, + tenant=tenants[0] + ), + WirelessLAN( + group=groups[0], + ssid='WLAN2', + status=WirelessLANStatusChoices.STATUS_ACTIVE, + tenant=tenants[0] + ), + WirelessLAN( + group=groups[0], + ssid='WLAN3', + status=WirelessLANStatusChoices.STATUS_ACTIVE, + tenant=tenants[0] + ), ) WirelessLAN.objects.bulk_create(wireless_lans) @@ -81,15 +96,16 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): cls.form_data = { 'ssid': 'WLAN2', 'group': groups[1].pk, + 'status': WirelessLANStatusChoices.STATUS_DISABLED, 'tenant': tenants[1].pk, 'tags': [t.pk for t in tags], } cls.csv_data = ( - f"group,ssid,tenant", - f"Wireless LAN Group 2,WLAN4,{tenants[0].name}", - f"Wireless LAN Group 2,WLAN5,{tenants[1].name}", - f"Wireless LAN Group 2,WLAN6,{tenants[2].name}", + f"group,ssid,status,tenant", + f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name}", + f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name}", + f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name}", ) cls.csv_update_data = ( @@ -100,6 +116,7 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) cls.bulk_edit_data = { + 'status': WirelessLANStatusChoices.STATUS_DISABLED, 'description': 'New description', }