From 8e1535f7ecba14aa9259b0ad62f0eabeccc12d82 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 12 Oct 2021 10:46:41 -0400 Subject: [PATCH 01/28] Add RF channel fields to Interface --- netbox/dcim/api/serializers.py | 10 +- netbox/dcim/choices.py | 131 ++++++++++++++++++++++ netbox/dcim/constants.py | 1 + netbox/dcim/filtersets.py | 5 +- netbox/dcim/forms/bulk_edit.py | 6 +- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/filtersets.py | 11 ++ netbox/dcim/forms/models.py | 5 +- netbox/dcim/forms/object_create.py | 15 ++- netbox/dcim/migrations/0136_wireless.py | 21 ++++ netbox/dcim/models/device_components.py | 18 +++ netbox/dcim/tables/devices.py | 4 +- netbox/templates/dcim/interface.html | 10 ++ netbox/templates/dcim/interface_edit.html | 10 ++ 14 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 netbox/dcim/migrations/0136_wireless.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d6e44c2810e..edd73b87e4e 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -632,6 +632,8 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co parent = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) + rf_channel = ChoiceField(choices=WirelessChannelChoices) + rf_channel_width = ChoiceField(choices=WirelessChannelWidthChoices) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), @@ -646,10 +648,10 @@ class Meta: model = Interface fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'wwn', 'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', - 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', - 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', - '_occupied', + 'wwn', 'mgmt_only', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'untagged_vlan', + 'tagged_vlans', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', + 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', 'count_ipaddresses', '_occupied', ] def validate(self, data): diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index acea294f886..9a78a74f9d4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1135,6 +1135,137 @@ class CableLengthUnitChoices(ChoiceSet): ) +# +# Wireless +# + +class WirelessChannelChoices(ChoiceSet): + CHANNEL_AUTO = 'auto' + + # 2.4 GHz + CHANNEL_24G_1 = '2.4g-1' + CHANNEL_24G_2 = '2.4g-2' + CHANNEL_24G_3 = '2.4g-3' + CHANNEL_24G_4 = '2.4g-4' + CHANNEL_24G_5 = '2.4g-5' + CHANNEL_24G_6 = '2.4g-6' + CHANNEL_24G_7 = '2.4g-7' + CHANNEL_24G_8 = '2.4g-8' + CHANNEL_24G_9 = '2.4g-9' + CHANNEL_24G_10 = '2.4g-10' + CHANNEL_24G_11 = '2.4g-11' + CHANNEL_24G_12 = '2.4g-12' + CHANNEL_24G_13 = '2.4g-13' + + # 5 GHz + CHANNEL_5G_32 = '5g-32' + CHANNEL_5G_34 = '5g-34' + CHANNEL_5G_36 = '5g-36' + CHANNEL_5G_38 = '5g-38' + CHANNEL_5G_40 = '5g-40' + CHANNEL_5G_42 = '5g-42' + CHANNEL_5G_44 = '5g-44' + CHANNEL_5G_46 = '5g-46' + CHANNEL_5G_48 = '5g-48' + CHANNEL_5G_50 = '5g-50' + CHANNEL_5G_52 = '5g-52' + CHANNEL_5G_54 = '5g-54' + CHANNEL_5G_56 = '5g-56' + CHANNEL_5G_58 = '5g-58' + CHANNEL_5G_60 = '5g-60' + CHANNEL_5G_62 = '5g-62' + CHANNEL_5G_64 = '5g-64' + CHANNEL_5G_100 = '5g-100' + CHANNEL_5G_102 = '5g-102' + CHANNEL_5G_104 = '5g-104' + CHANNEL_5G_106 = '5g-106' + CHANNEL_5G_108 = '5g-108' + CHANNEL_5G_110 = '5g-110' + CHANNEL_5G_112 = '5g-112' + CHANNEL_5G_114 = '5g-114' + CHANNEL_5G_116 = '5g-116' + CHANNEL_5G_118 = '5g-118' + CHANNEL_5G_120 = '5g-120' + CHANNEL_5G_122 = '5g-122' + CHANNEL_5G_124 = '5g-124' + CHANNEL_5G_126 = '5g-126' + CHANNEL_5G_128 = '5g-128' + + CHOICES = ( + (CHANNEL_AUTO, 'Auto'), + ( + '2.4 GHz (802.11b/g/n/ax)', + ( + (CHANNEL_24G_1, '1 (2412 MHz)'), + (CHANNEL_24G_2, '2 (2417 MHz)'), + (CHANNEL_24G_3, '3 (2422 MHz)'), + (CHANNEL_24G_4, '4 (2427 MHz)'), + (CHANNEL_24G_5, '5 (2432 MHz)'), + (CHANNEL_24G_6, '6 (2437 MHz)'), + (CHANNEL_24G_7, '7 (2442 MHz)'), + (CHANNEL_24G_8, '8 (2447 MHz)'), + (CHANNEL_24G_9, '9 (2452 MHz)'), + (CHANNEL_24G_10, '10 (2457 MHz)'), + (CHANNEL_24G_11, '11 (2462 MHz)'), + (CHANNEL_24G_12, '12 (2467 MHz)'), + (CHANNEL_24G_13, '13 (2472 MHz)'), + ) + ), + ( + '5 GHz (802.11a/n/ac/ax)', + ( + (CHANNEL_5G_32, '32 (5160 MHz)'), + (CHANNEL_5G_34, '34 (5170 MHz)'), + (CHANNEL_5G_36, '36 (5180 MHz)'), + (CHANNEL_5G_38, '38 (5190 MHz)'), + (CHANNEL_5G_40, '40 (5200 MHz)'), + (CHANNEL_5G_42, '42 (5210 MHz)'), + (CHANNEL_5G_44, '44 (5220 MHz)'), + (CHANNEL_5G_46, '46 (5230 MHz)'), + (CHANNEL_5G_48, '48 (5240 MHz)'), + (CHANNEL_5G_50, '50 (5250 MHz)'), + (CHANNEL_5G_52, '52 (5260 MHz)'), + (CHANNEL_5G_54, '54 (5270 MHz)'), + (CHANNEL_5G_56, '56 (5280 MHz)'), + (CHANNEL_5G_58, '58 (5290 MHz)'), + (CHANNEL_5G_60, '60 (5300 MHz)'), + (CHANNEL_5G_62, '62 (5310 MHz)'), + (CHANNEL_5G_64, '64 (5320 MHz)'), + (CHANNEL_5G_100, '100 (5500 MHz)'), + (CHANNEL_5G_102, '102 (5510 MHz)'), + (CHANNEL_5G_104, '104 (5520 MHz)'), + (CHANNEL_5G_106, '106 (5530 MHz)'), + (CHANNEL_5G_108, '108 (5540 MHz)'), + (CHANNEL_5G_110, '110 (5550 MHz)'), + (CHANNEL_5G_112, '112 (5560 MHz)'), + (CHANNEL_5G_114, '114 (5570 MHz)'), + (CHANNEL_5G_116, '116 (5580 MHz)'), + (CHANNEL_5G_118, '118 (5590 MHz)'), + (CHANNEL_5G_120, '120 (5600 MHz)'), + (CHANNEL_5G_122, '122 (5610 MHz)'), + (CHANNEL_5G_124, '124 (5620 MHz)'), + (CHANNEL_5G_126, '126 (5630 MHz)'), + (CHANNEL_5G_128, '128 (5640 MHz)'), + ) + ), + ) + + +class WirelessChannelWidthChoices(ChoiceSet): + + CHANNEL_WIDTH_20 = 20 + CHANNEL_WIDTH_40 = 40 + CHANNEL_WIDTH_80 = 80 + CHANNEL_WIDTH_160 = 160 + + CHOICES = ( + (CHANNEL_WIDTH_20, '20 MHz'), + (CHANNEL_WIDTH_40, '40 MHz'), + (CHANNEL_WIDTH_80, '80 MHz'), + (CHANNEL_WIDTH_160, '160 MHz'), + ) + + # # PowerFeeds # diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 2a4d368f4e2..0d64b357b0f 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -42,6 +42,7 @@ InterfaceTypeChoices.TYPE_80211N, InterfaceTypeChoices.TYPE_80211AC, InterfaceTypeChoices.TYPE_80211AD, + InterfaceTypeChoices.TYPE_80211AX, ] NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 7f029097ee6..0c756957a89 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -990,7 +990,10 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT class Meta: model = Interface - fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] + fields = [ + 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_channel', 'rf_channel_width', + 'description', + ] def filter_device(self, queryset, name, value): try: diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index fd87d7304b5..67a482a266d 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -926,7 +926,7 @@ def __init__(self, *args, **kwargs): class InterfaceBulkEditForm( form_from_model(Interface, [ 'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', - 'mode', + 'mode', 'rf_channel', 'rf_channel_width', ]), BootstrapMixin, AddRemoveTagsForm, @@ -977,8 +977,8 @@ class InterfaceBulkEditForm( class Meta: nullable_fields = [ - 'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'untagged_vlan', - 'tagged_vlans', + 'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel', + 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index ff9ab6fff3e..a2685c8e0a0 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -584,7 +584,7 @@ class Meta: model = Interface fields = ( 'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn', - 'mtu', 'mgmt_only', 'description', 'mode', + 'mtu', 'mgmt_only', 'description', 'mode', 'rf_channel', 'rf_channel_width', ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4f4e10e96cb..605139c1bea 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -963,6 +963,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'], + ['rf_channel', 'rf_channel_width'], ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( @@ -990,6 +991,16 @@ class InterfaceFilterForm(DeviceComponentFilterForm): required=False, label='WWN' ) + rf_channel = forms.MultipleChoiceField( + choices=WirelessChannelChoices, + required=False, + widget=StaticSelectMultiple() + ) + rf_channel_width = forms.MultipleChoiceField( + choices=WirelessChannelWidthChoices, + required=False, + widget=StaticSelectMultiple() + ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index a8c2991a45e..435fab3090e 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1098,12 +1098,15 @@ class Meta: model = Interface fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', - 'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', + 'mark_connected', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', + 'tags', ] widgets = { 'device': forms.HiddenInput(), 'type': StaticSelect(), 'mode': StaticSelect(), + 'rf_channel': StaticSelect(), + 'rf_channel_width': StaticSelect(), } labels = { 'mode': '802.1Q Mode', diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 7577ad3553f..db28412e6ce 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -465,7 +465,19 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): mode = forms.ChoiceField( choices=add_blank_choice(InterfaceModeChoices), required=False, + widget=StaticSelect() + ) + rf_channel = forms.ChoiceField( + choices=add_blank_choice(WirelessChannelChoices), + required=False, + widget=StaticSelect(), + label='Wireless channel' + ) + rf_channel_width = forms.ChoiceField( + choices=add_blank_choice(WirelessChannelWidthChoices), + required=False, widget=StaticSelect(), + label='Channel width' ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), @@ -477,7 +489,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): ) field_order = ( 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags' + 'description', 'mgmt_only', 'mark_connected', 'rf_channel', 'rf_channel_width', 'mode' 'untagged_vlan', + 'tagged_vlans', 'tags' ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/migrations/0136_wireless.py b/netbox/dcim/migrations/0136_wireless.py new file mode 100644 index 00000000000..429a72694e1 --- /dev/null +++ b/netbox/dcim/migrations/0136_wireless.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0135_location_tenant'), + ] + + operations = [ + migrations.AddField( + model_name='interface', + name='rf_channel', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interface', + name='rf_channel_width', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 386776b4195..4e0d65f86be 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -517,6 +517,18 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): verbose_name='WWN', help_text='64-bit World Wide Name' ) + rf_channel = models.CharField( + max_length=50, + choices=WirelessChannelChoices, + blank=True, + verbose_name='Wireless channel' + ) + rf_channel_width = models.PositiveSmallIntegerField( + choices=WirelessChannelWidthChoices, + blank=True, + null=True, + verbose_name='Channel width' + ) untagged_vlan = models.ForeignKey( to='ipam.VLAN', on_delete=models.SET_NULL, @@ -603,6 +615,12 @@ def clean(self): if self.pk and self.lag_id == self.pk: raise ValidationError({'lag': "A LAG interface cannot be its own parent."}) + # RF channel attributes may be set only for wireless interfaces + if self.rf_channel and self.type not in WIRELESS_IFACE_TYPES: + raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."}) + if self.rf_channel_width and self.type not in WIRELESS_IFACE_TYPES: + raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."}) + # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]: raise ValidationError({ diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index c2b4b907bef..1eae62a05e8 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -493,8 +493,8 @@ class Meta(DeviceComponentTable.Meta): model = Interface fields = ( 'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', - 'untagged_vlan', 'tagged_vlans', + 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', + 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index f9a9b04250d..3283aac4fe1 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -39,6 +39,16 @@
Type {{ object.get_type_display }} + {% if object.is_wireless %} + + Channel + {{ object.get_rf_channel_display|placeholder }} + + + Channel Width + {{ object.get_rf_channel_width_display|placeholder }} + + {% endif %} Enabled diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index 041eab73a83..e91c74d31fe 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -29,6 +29,16 @@
Interface
{% render_field form.mark_connected %} + {% if form.instance.is_wireless %} +
+
+
Wireless
+
+ {% render_field form.rf_channel %} + {% render_field form.rf_channel_width %} +
+ {% endif %} +
802.1Q Switching
From 8b80b0c3df451a894c1286f7afdbb5cd940b8f61 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 12 Oct 2021 12:27:12 -0400 Subject: [PATCH 02/28] Introduce the wireless app and SSID model --- netbox/netbox/api/views.py | 1 + netbox/netbox/graphql/schema.py | 2 + netbox/netbox/navigation_menu.py | 14 +++++++ netbox/netbox/settings.py | 1 + netbox/netbox/urls.py | 2 + netbox/templates/wireless/ssid.html | 46 ++++++++++++++++++++++ netbox/wireless/__init__.py | 0 netbox/wireless/api/__init__.py | 0 netbox/wireless/api/nested_serializers.py | 16 ++++++++ netbox/wireless/api/serializers.py | 21 ++++++++++ netbox/wireless/api/urls.py | 12 ++++++ netbox/wireless/api/views.py | 24 +++++++++++ netbox/wireless/apps.py | 5 +++ netbox/wireless/filtersets.py | 31 +++++++++++++++ netbox/wireless/forms/__init__.py | 4 ++ netbox/wireless/forms/bulk_edit.py | 29 ++++++++++++++ netbox/wireless/forms/bulk_import.py | 20 ++++++++++ netbox/wireless/forms/filtersets.py | 19 +++++++++ netbox/wireless/forms/models.py | 32 +++++++++++++++ netbox/wireless/graphql/__init__.py | 0 netbox/wireless/graphql/schema.py | 9 +++++ netbox/wireless/graphql/types.py | 14 +++++++ netbox/wireless/migrations/0001_initial.py | 36 +++++++++++++++++ netbox/wireless/migrations/__init__.py | 0 netbox/wireless/models.py | 40 +++++++++++++++++++ netbox/wireless/tables.py | 24 +++++++++++ netbox/wireless/urls.py | 22 +++++++++++ netbox/wireless/views.py | 46 ++++++++++++++++++++++ 28 files changed, 470 insertions(+) create mode 100644 netbox/templates/wireless/ssid.html create mode 100644 netbox/wireless/__init__.py create mode 100644 netbox/wireless/api/__init__.py create mode 100644 netbox/wireless/api/nested_serializers.py create mode 100644 netbox/wireless/api/serializers.py create mode 100644 netbox/wireless/api/urls.py create mode 100644 netbox/wireless/api/views.py create mode 100644 netbox/wireless/apps.py create mode 100644 netbox/wireless/filtersets.py create mode 100644 netbox/wireless/forms/__init__.py create mode 100644 netbox/wireless/forms/bulk_edit.py create mode 100644 netbox/wireless/forms/bulk_import.py create mode 100644 netbox/wireless/forms/filtersets.py create mode 100644 netbox/wireless/forms/models.py create mode 100644 netbox/wireless/graphql/__init__.py create mode 100644 netbox/wireless/graphql/schema.py create mode 100644 netbox/wireless/graphql/types.py create mode 100644 netbox/wireless/migrations/0001_initial.py create mode 100644 netbox/wireless/migrations/__init__.py create mode 100644 netbox/wireless/models.py create mode 100644 netbox/wireless/tables.py create mode 100644 netbox/wireless/urls.py create mode 100644 netbox/wireless/views.py diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 74000e97800..7ad64aeae2e 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -308,6 +308,7 @@ def get(self, request, format=None): ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)), ('users', reverse('users-api:api-root', request=request, format=format)), ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)), + ('wireless', reverse('wireless-api:api-root', request=request, format=format)), ))) diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index bb752b8c44d..812c1656d07 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -7,6 +7,7 @@ from tenancy.graphql.schema import TenancyQuery from users.graphql.schema import UsersQuery from virtualization.graphql.schema import VirtualizationQuery +from wireless.graphql.schema import WirelessQuery class Query( @@ -17,6 +18,7 @@ class Query( TenancyQuery, UsersQuery, VirtualizationQuery, + WirelessQuery, graphene.ObjectType ): pass diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation_menu.py index a3978f16e35..0a78f35abfd 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation_menu.py @@ -188,6 +188,19 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): ), ) +WIRELESS_MENU = Menu( + label='Wireless', + icon_class='mdi mdi-wifi', + groups=( + MenuGroup( + label='Wireless', + items=( + get_model_item('wireless', 'ssid', 'SSIDs'), + ), + ), + ), +) + IPAM_MENU = Menu( label='IPAM', icon_class='mdi mdi-counter', @@ -343,6 +356,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): ORGANIZATION_MENU, DEVICES_MENU, CONNECTIONS_MENU, + WIRELESS_MENU, IPAM_MENU, VIRTUALIZATION_MENU, CIRCUITS_MENU, diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 3df9a855ac6..e41c77d1d2c 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -326,6 +326,7 @@ def _setting(name, default=None): 'users', 'utilities', 'virtualization', + 'wireless', 'django_rq', # Must come after extras to allow overriding management commands 'drf_yasg', ] diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 3d4c60c93bb..4e0a2e2c662 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -48,6 +48,7 @@ path('tenancy/', include('tenancy.urls')), path('user/', include('users.urls')), path('virtualization/', include('virtualization.urls')), + path('wireless/', include('wireless.urls')), # API path('api/', APIRootView.as_view(), name='api-root'), @@ -58,6 +59,7 @@ path('api/tenancy/', include('tenancy.api.urls')), path('api/users/', include('users.api.urls')), path('api/virtualization/', include('virtualization.api.urls')), + path('api/wireless/', include('wireless.api.urls')), path('api/status/', StatusView.as_view(), name='api-status'), path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'), path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'), diff --git a/netbox/templates/wireless/ssid.html b/netbox/templates/wireless/ssid.html new file mode 100644 index 00000000000..5425149aafb --- /dev/null +++ b/netbox/templates/wireless/ssid.html @@ -0,0 +1,46 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} + +{% block content %} +
+
+
+
SSID
+
+ + + + + + + + + + + + + +
Name{{ object.name }}
Description{{ object.description|placeholder }}
VLAN + {% if object.vlan %} + {{ object.vlan }} + {% else %} + None + {% endif %} +
+
+
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %} + {% plugin_left_page object %} +
+
+ {% include 'inc/custom_fields_panel.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox/wireless/__init__.py b/netbox/wireless/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/netbox/wireless/api/__init__.py b/netbox/wireless/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/netbox/wireless/api/nested_serializers.py b/netbox/wireless/api/nested_serializers.py new file mode 100644 index 00000000000..50454a64174 --- /dev/null +++ b/netbox/wireless/api/nested_serializers.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from netbox.api import WritableNestedSerializer +from wireless.models import * + +__all__ = ( + 'NestedSSIDSerializer', +) + + +class NestedSSIDSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail') + + class Meta: + model = SSID + fields = ['id', 'url', 'display', 'name'] diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py new file mode 100644 index 00000000000..c129e5c9682 --- /dev/null +++ b/netbox/wireless/api/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from dcim.api.serializers import NestedInterfaceSerializer +from ipam.api.serializers import NestedVLANSerializer +from netbox.api.serializers import PrimaryModelSerializer +from wireless.models import * + +__all__ = ( + 'SSIDSerializer', +) + + +class SSIDSerializer(PrimaryModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail') + vlan = NestedVLANSerializer(required=False, allow_null=True) + + class Meta: + model = SSID + fields = [ + 'id', 'url', 'display', 'name', 'description', 'vlan', + ] diff --git a/netbox/wireless/api/urls.py b/netbox/wireless/api/urls.py new file mode 100644 index 00000000000..f6936708ce1 --- /dev/null +++ b/netbox/wireless/api/urls.py @@ -0,0 +1,12 @@ +from netbox.api import OrderedDefaultRouter +from . import views + + +router = OrderedDefaultRouter() +router.APIRootView = views.WirelessRootView + +# SSIDs +router.register('ssids', views.SSIDViewSet) + +app_name = 'wireless-api' +urlpatterns = router.urls diff --git a/netbox/wireless/api/views.py b/netbox/wireless/api/views.py new file mode 100644 index 00000000000..97827eb7eec --- /dev/null +++ b/netbox/wireless/api/views.py @@ -0,0 +1,24 @@ +from rest_framework.routers import APIRootView + +from extras.api.views import CustomFieldModelViewSet +from wireless import filtersets +from wireless.models import * +from . import serializers + + +class WirelessRootView(APIRootView): + """ + Wireless API root view + """ + def get_view_name(self): + return 'Wireless' + + +# +# Providers +# + +class SSIDViewSet(CustomFieldModelViewSet): + queryset = SSID.objects.prefetch_related('tags') + serializer_class = serializers.SSIDSerializer + filterset_class = filtersets.SSIDFilterSet diff --git a/netbox/wireless/apps.py b/netbox/wireless/apps.py new file mode 100644 index 00000000000..1f6deff2298 --- /dev/null +++ b/netbox/wireless/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class WirelessConfig(AppConfig): + name = 'wireless' diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py new file mode 100644 index 00000000000..232bc74ff72 --- /dev/null +++ b/netbox/wireless/filtersets.py @@ -0,0 +1,31 @@ +import django_filters +from django.db.models import Q + +from extras.filters import TagFilter +from netbox.filtersets import PrimaryModelFilterSet +from .models import * + +__all__ = ( + 'SSIDFilterSet', +) + + +class SSIDFilterSet(PrimaryModelFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + tag = TagFilter() + + class Meta: + model = SSID + fields = ['id', 'name'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = ( + Q(name__icontains=value) | + Q(description__icontains=value) + ) + return queryset.filter(qs_filter) diff --git a/netbox/wireless/forms/__init__.py b/netbox/wireless/forms/__init__.py new file mode 100644 index 00000000000..62c2ec2d918 --- /dev/null +++ b/netbox/wireless/forms/__init__.py @@ -0,0 +1,4 @@ +from .models import * +from .filtersets import * +from .bulk_edit import * +from .bulk_import import * diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py new file mode 100644 index 00000000000..ed9fb650bf7 --- /dev/null +++ b/netbox/wireless/forms/bulk_edit.py @@ -0,0 +1,29 @@ +from django import forms + +from dcim.models import * +from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from ipam.models import VLAN +from utilities.forms import BootstrapMixin, DynamicModelChoiceField + +__all__ = ( + 'SSIDBulkEditForm', +) + + +class SSIDBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerFeed.objects.all(), + widget=forms.MultipleHiddenInput + ) + vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + ) + description = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = [ + 'vlan', 'description', + ] diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py new file mode 100644 index 00000000000..0cf997fd357 --- /dev/null +++ b/netbox/wireless/forms/bulk_import.py @@ -0,0 +1,20 @@ +from extras.forms import CustomFieldModelCSVForm +from ipam.models import VLAN +from utilities.forms import CSVModelChoiceField +from wireless.models import SSID + +__all__ = ( + 'SSIDCSVForm', +) + + +class SSIDCSVForm(CustomFieldModelCSVForm): + vlan = CSVModelChoiceField( + queryset=VLAN.objects.all(), + to_field_name='name', + help_text='Bridged VLAN' + ) + + class Meta: + model = SSID + fields = ('name', 'description', 'vlan') diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py new file mode 100644 index 00000000000..733b807f74b --- /dev/null +++ b/netbox/wireless/forms/filtersets.py @@ -0,0 +1,19 @@ +from django import forms +from django.utils.translation import gettext as _ + +from dcim.models import * +from extras.forms import CustomFieldModelFilterForm +from utilities.forms import BootstrapMixin, TagFilterField + + +class SSIDFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = PowerFeed + field_groups = [ + ['q', 'tag'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + tag = TagFilterField(model) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py new file mode 100644 index 00000000000..ea6d51223b6 --- /dev/null +++ b/netbox/wireless/forms/models.py @@ -0,0 +1,32 @@ +from dcim.constants import * +from dcim.models import * +from extras.forms import CustomFieldModelForm +from extras.models import Tag +from ipam.models import VLAN +from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from wireless.models import SSID + +__all__ = ( + 'SSIDForm', +) + + +class SSIDForm(BootstrapMixin, CustomFieldModelForm): + vlan = DynamicModelChoiceField( + queryset=VLAN.objects.all(), + required=False + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = SSID + fields = [ + 'name', 'description', 'vlan', 'tags', + ] + fieldsets = ( + ('SSID', ('name', 'description', 'tags')), + ('VLAN', ('vlan',)), + ) diff --git a/netbox/wireless/graphql/__init__.py b/netbox/wireless/graphql/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py new file mode 100644 index 00000000000..d0beec7d9f7 --- /dev/null +++ b/netbox/wireless/graphql/schema.py @@ -0,0 +1,9 @@ +import graphene + +from netbox.graphql.fields import ObjectField, ObjectListField +from .types import * + + +class WirelessQuery(graphene.ObjectType): + ssid = ObjectField(SSIDType) + ssid_list = ObjectListField(SSIDType) diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py new file mode 100644 index 00000000000..66e73429ddc --- /dev/null +++ b/netbox/wireless/graphql/types.py @@ -0,0 +1,14 @@ +from wireless import filtersets, models +from netbox.graphql.types import ObjectType + +__all__ = ( + 'SSIDType', +) + + +class SSIDType(ObjectType): + + class Meta: + model = models.SSID + fields = '__all__' + filterset_class = filtersets.SSIDFilterSet diff --git a/netbox/wireless/migrations/0001_initial.py b/netbox/wireless/migrations/0001_initial.py new file mode 100644 index 00000000000..b0011dad900 --- /dev/null +++ b/netbox/wireless/migrations/0001_initial.py @@ -0,0 +1,36 @@ +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('dcim', '0136_wireless'), + ('extras', '0062_clear_secrets_changelog'), + ('ipam', '0050_iprange'), + ] + + operations = [ + migrations.CreateModel( + name='SSID', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=32)), + ('description', models.CharField(blank=True, max_length=200)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')), + ], + options={ + 'verbose_name': 'SSID', + 'verbose_name_plural': 'SSIDs', + 'ordering': ('name', 'pk'), + }, + ), + ] diff --git a/netbox/wireless/migrations/__init__.py b/netbox/wireless/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py new file mode 100644 index 00000000000..5bb96434556 --- /dev/null +++ b/netbox/wireless/models.py @@ -0,0 +1,40 @@ +from django.db import models + +from extras.utils import extras_features +from netbox.models import PrimaryModel +from utilities.querysets import RestrictedQuerySet + +__all__ = ( + 'SSID', +) + + +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') +class SSID(PrimaryModel): + """ + A service set identifier belonging to a wireless network. + """ + name = models.CharField( + max_length=32 + ) + vlan = models.ForeignKey( + to='ipam.VLAN', + on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name='VLAN' + ) + description = models.CharField( + max_length=200, + blank=True + ) + + objects = RestrictedQuerySet.as_manager() + + class Meta: + ordering = ('name', 'pk') + verbose_name = 'SSID' + verbose_name_plural = 'SSIDs' + + def __str__(self): + return self.name diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py new file mode 100644 index 00000000000..846296bb47d --- /dev/null +++ b/netbox/wireless/tables.py @@ -0,0 +1,24 @@ +import django_tables2 as tables + +from .models import SSID +from utilities.tables import BaseTable, TagColumn, ToggleColumn + +__all__ = ( + 'SSIDTable', +) + + +class SSIDTable(BaseTable): + pk = ToggleColumn() + id = tables.Column( + linkify=True, + verbose_name='ID' + ) + tags = TagColumn( + url_name='dcim:cable_list' + ) + + class Meta(BaseTable.Meta): + model = SSID + fields = ('pk', 'id', 'name', 'description', 'vlan') + default_columns = ('pk', 'name', 'description', 'vlan') diff --git a/netbox/wireless/urls.py b/netbox/wireless/urls.py new file mode 100644 index 00000000000..57e0eab9b1f --- /dev/null +++ b/netbox/wireless/urls.py @@ -0,0 +1,22 @@ +from django.urls import path + +from extras.views import ObjectChangeLogView, ObjectJournalView +from . import views +from .models import * + +app_name = 'wireless' +urlpatterns = ( + + # SSIDs + path('ssids/', views.SSIDListView.as_view(), name='ssid_list'), + path('ssids/add/', views.SSIDEditView.as_view(), name='ssid_add'), + path('ssids/import/', views.SSIDBulkImportView.as_view(), name='ssid_import'), + path('ssids/edit/', views.SSIDBulkEditView.as_view(), name='ssid_bulk_edit'), + path('ssids/delete/', views.SSIDBulkDeleteView.as_view(), name='ssid_bulk_delete'), + path('ssids//', views.SSIDView.as_view(), name='ssid'), + path('ssids//edit/', views.SSIDEditView.as_view(), name='ssid_edit'), + path('ssids//delete/', views.SSIDDeleteView.as_view(), name='ssid_delete'), + path('ssids//changelog/', ObjectChangeLogView.as_view(), name='ssid_changelog', kwargs={'model': SSID}), + path('ssids//journal/', ObjectJournalView.as_view(), name='ssid_journal', kwargs={'model': SSID}), + +) diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py new file mode 100644 index 00000000000..b0d1f51560c --- /dev/null +++ b/netbox/wireless/views.py @@ -0,0 +1,46 @@ +from netbox.views import generic +from . import filtersets, forms, tables +from .models import * + + +# +# SSIDs +# + +class SSIDListView(generic.ObjectListView): + queryset = SSID.objects.all() + filterset = filtersets.SSIDFilterSet + filterset_form = forms.SSIDFilterForm + table = tables.SSIDTable + + +class SSIDView(generic.ObjectView): + queryset = SSID.objects.prefetch_related('power_panel', 'rack') + + +class SSIDEditView(generic.ObjectEditView): + queryset = SSID.objects.all() + model_form = forms.SSIDForm + + +class SSIDDeleteView(generic.ObjectDeleteView): + queryset = SSID.objects.all() + + +class SSIDBulkImportView(generic.BulkImportView): + queryset = SSID.objects.all() + model_form = forms.SSIDCSVForm + table = tables.SSIDTable + + +class SSIDBulkEditView(generic.BulkEditView): + queryset = SSID.objects.prefetch_related('power_panel', 'rack') + filterset = filtersets.SSIDFilterSet + table = tables.SSIDTable + form = forms.SSIDBulkEditForm + + +class SSIDBulkDeleteView(generic.BulkDeleteView): + queryset = SSID.objects.prefetch_related('power_panel', 'rack') + filterset = filtersets.SSIDFilterSet + table = tables.SSIDTable From 38f6d22d2d7ac412efa9b50ef4f598d17726e0a9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 12 Oct 2021 13:48:06 -0400 Subject: [PATCH 03/28] Enable attachment of wireless interfaces to SSIDs --- netbox/dcim/forms/models.py | 10 ++++++-- netbox/dcim/migrations/0136_wireless.py | 6 +++++ netbox/dcim/models/device_components.py | 6 +++++ netbox/templates/dcim/interface.html | 27 ++++++++++++++++++++++ netbox/templates/dcim/interface_edit.html | 1 + netbox/wireless/migrations/0001_initial.py | 1 - netbox/wireless/models.py | 10 +++++++- netbox/wireless/tables.py | 7 +++--- netbox/wireless/views.py | 6 ++--- 9 files changed, 63 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 435fab3090e..2a6dc1f6f73 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -16,6 +16,7 @@ SlugField, StaticSelect, ) from virtualization.models import Cluster, ClusterGroup +from wireless.models import SSID from .common import InterfaceCommonForm __all__ = ( @@ -1068,6 +1069,11 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): 'type': 'lag', } ) + ssids = DynamicModelMultipleChoiceField( + queryset=SSID.objects.all(), + required=False, + label='SSIDs' + ) vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), required=False, @@ -1098,8 +1104,8 @@ class Meta: model = Interface fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', - 'mark_connected', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', - 'tags', + 'mark_connected', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'ssids', 'untagged_vlan', + 'tagged_vlans', 'tags', ] widgets = { 'device': forms.HiddenInput(), diff --git a/netbox/dcim/migrations/0136_wireless.py b/netbox/dcim/migrations/0136_wireless.py index 429a72694e1..0a1d15365c5 100644 --- a/netbox/dcim/migrations/0136_wireless.py +++ b/netbox/dcim/migrations/0136_wireless.py @@ -4,6 +4,7 @@ class Migration(migrations.Migration): dependencies = [ + ('wireless', '__first__'), ('dcim', '0135_location_tenant'), ] @@ -18,4 +19,9 @@ class Migration(migrations.Migration): name='rf_channel_width', field=models.PositiveSmallIntegerField(blank=True, null=True), ), + migrations.AddField( + model_name='interface', + name='ssids', + field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.SSID'), + ), ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 4e0d65f86be..5e3e7e6dbc5 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -529,6 +529,12 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): null=True, verbose_name='Channel width' ) + ssids = models.ManyToManyField( + to='wireless.SSID', + related_name='interfaces', + blank=True, + verbose_name='SSIDs' + ) untagged_vlan = models.ForeignKey( to='ipam.VLAN', on_delete=models.SET_NULL, diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 3283aac4fe1..bc4fc23e2dc 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -258,6 +258,33 @@
{% endif %} + {% if object.is_wireless %} +
+
SSIDs
+
+ + + + + + + + {% for ssid in object.ssids.all %} + + + + {% empty %} + + + + {% endfor %} + +
Name
+ {{ ssid.name }} +
None
+
+
+ {% endif %} {% if object.is_lag %}
LAG Members
diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index e91c74d31fe..9fc7524323c 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -36,6 +36,7 @@
Wireless
{% render_field form.rf_channel %} {% render_field form.rf_channel_width %} + {% render_field form.ssids %} {% endif %} diff --git a/netbox/wireless/migrations/0001_initial.py b/netbox/wireless/migrations/0001_initial.py index b0011dad900..78d1dfc7379 100644 --- a/netbox/wireless/migrations/0001_initial.py +++ b/netbox/wireless/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dcim', '0136_wireless'), ('extras', '0062_clear_secrets_changelog'), ('ipam', '0050_iprange'), ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 5bb96434556..2bdcecd79af 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -1,7 +1,8 @@ from django.db import models +from django.urls import reverse from extras.utils import extras_features -from netbox.models import PrimaryModel +from netbox.models import BigIDModel, PrimaryModel from utilities.querysets import RestrictedQuerySet __all__ = ( @@ -9,6 +10,10 @@ ) +# +# SSIDs +# + @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class SSID(PrimaryModel): """ @@ -38,3 +43,6 @@ class Meta: def __str__(self): return self.name + + def get_absolute_url(self): + return reverse('wireless:ssid', args=[self.pk]) diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 846296bb47d..9d37055495e 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -10,9 +10,8 @@ class SSIDTable(BaseTable): pk = ToggleColumn() - id = tables.Column( - linkify=True, - verbose_name='ID' + name = tables.Column( + linkify=True ) tags = TagColumn( url_name='dcim:cable_list' @@ -20,5 +19,5 @@ class SSIDTable(BaseTable): class Meta(BaseTable.Meta): model = SSID - fields = ('pk', 'id', 'name', 'description', 'vlan') + fields = ('pk', 'name', 'description', 'vlan') default_columns = ('pk', 'name', 'description', 'vlan') diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index b0d1f51560c..b741330b73d 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -15,7 +15,7 @@ class SSIDListView(generic.ObjectListView): class SSIDView(generic.ObjectView): - queryset = SSID.objects.prefetch_related('power_panel', 'rack') + queryset = SSID.objects.all() class SSIDEditView(generic.ObjectEditView): @@ -34,13 +34,13 @@ class SSIDBulkImportView(generic.BulkImportView): class SSIDBulkEditView(generic.BulkEditView): - queryset = SSID.objects.prefetch_related('power_panel', 'rack') + queryset = SSID.objects.all() filterset = filtersets.SSIDFilterSet table = tables.SSIDTable form = forms.SSIDBulkEditForm class SSIDBulkDeleteView(generic.BulkDeleteView): - queryset = SSID.objects.prefetch_related('power_panel', 'rack') + queryset = SSID.objects.all() filterset = filtersets.SSIDFilterSet table = tables.SSIDTable From 5271680483709114760dc0695e3d1dbbb6f45186 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 12 Oct 2021 17:02:53 -0400 Subject: [PATCH 04/28] Rename SSID model to WirelessLAN --- netbox/dcim/forms/models.py | 10 ++-- netbox/dcim/migrations/0136_wireless.py | 6 +-- netbox/dcim/models/device_components.py | 6 +-- netbox/netbox/navigation_menu.py | 2 +- netbox/templates/dcim/interface.html | 8 +-- netbox/templates/dcim/interface_edit.html | 2 +- .../wireless/{ssid.html => wirelesslan.html} | 6 +-- netbox/wireless/api/nested_serializers.py | 8 +-- netbox/wireless/api/serializers.py | 9 ++-- netbox/wireless/api/urls.py | 3 +- netbox/wireless/api/views.py | 8 +-- netbox/wireless/constants.py | 1 + netbox/wireless/filtersets.py | 10 ++-- netbox/wireless/forms/bulk_edit.py | 4 +- netbox/wireless/forms/bulk_import.py | 10 ++-- netbox/wireless/forms/filtersets.py | 6 +-- netbox/wireless/forms/models.py | 14 +++-- netbox/wireless/graphql/schema.py | 4 +- netbox/wireless/graphql/types.py | 8 +-- netbox/wireless/migrations/0001_initial.py | 11 ++-- netbox/wireless/models.py | 19 +++---- netbox/wireless/tables.py | 16 +++--- netbox/wireless/urls.py | 22 ++++---- netbox/wireless/views.py | 52 +++++++++---------- 24 files changed, 119 insertions(+), 126 deletions(-) rename netbox/templates/wireless/{ssid.html => wirelesslan.html} (90%) create mode 100644 netbox/wireless/constants.py diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 2a6dc1f6f73..cd697e9f3e4 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -16,7 +16,7 @@ SlugField, StaticSelect, ) from virtualization.models import Cluster, ClusterGroup -from wireless.models import SSID +from wireless.models import WirelessLAN from .common import InterfaceCommonForm __all__ = ( @@ -1069,10 +1069,10 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): 'type': 'lag', } ) - ssids = DynamicModelMultipleChoiceField( - queryset=SSID.objects.all(), + wireless_lans = DynamicModelMultipleChoiceField( + queryset=WirelessLAN.objects.all(), required=False, - label='SSIDs' + label='Wireless LANs' ) vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), @@ -1104,7 +1104,7 @@ class Meta: model = Interface fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', - 'mark_connected', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'ssids', 'untagged_vlan', + 'mark_connected', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags', ] widgets = { diff --git a/netbox/dcim/migrations/0136_wireless.py b/netbox/dcim/migrations/0136_wireless.py index 0a1d15365c5..108e638028d 100644 --- a/netbox/dcim/migrations/0136_wireless.py +++ b/netbox/dcim/migrations/0136_wireless.py @@ -4,7 +4,7 @@ class Migration(migrations.Migration): dependencies = [ - ('wireless', '__first__'), + ('wireless', '0001_initial'), ('dcim', '0135_location_tenant'), ] @@ -21,7 +21,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='interface', - name='ssids', - field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.SSID'), + name='wireless_lans', + field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.WirelessLAN'), ), ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 5e3e7e6dbc5..60eb4c368f1 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -529,11 +529,11 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): null=True, verbose_name='Channel width' ) - ssids = models.ManyToManyField( - to='wireless.SSID', + wireless_lans = models.ManyToManyField( + to='wireless.WirelessLAN', related_name='interfaces', blank=True, - verbose_name='SSIDs' + verbose_name='Wireless LANs' ) untagged_vlan = models.ForeignKey( to='ipam.VLAN', diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation_menu.py index 0a78f35abfd..073189d31ae 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation_menu.py @@ -195,7 +195,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): MenuGroup( label='Wireless', items=( - get_model_item('wireless', 'ssid', 'SSIDs'), + get_model_item('wireless', 'wirelesslan', 'Wireless LANs'), ), ), ), diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index bc4fc23e2dc..33eaa95dbb4 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -260,19 +260,19 @@
{% endif %} {% if object.is_wireless %}
-
SSIDs
+
Wireless LANs
- + - {% for ssid in object.ssids.all %} + {% for wlan in object.wlans.all %} {% empty %} diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index 9fc7524323c..51834f4e2f8 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -36,7 +36,7 @@
Wireless
{% render_field form.rf_channel %} {% render_field form.rf_channel_width %} - {% render_field form.ssids %} + {% render_field form.wireless_lans %} {% endif %} diff --git a/netbox/templates/wireless/ssid.html b/netbox/templates/wireless/wirelesslan.html similarity index 90% rename from netbox/templates/wireless/ssid.html rename to netbox/templates/wireless/wirelesslan.html index 5425149aafb..98bde8688d9 100644 --- a/netbox/templates/wireless/ssid.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -6,12 +6,12 @@
-
SSID
+
Wireless LAN
NameSSID
- {{ ssid.name }} + {{ wlan.ssid }}
- - + + diff --git a/netbox/wireless/api/nested_serializers.py b/netbox/wireless/api/nested_serializers.py index 50454a64174..e290653a2a6 100644 --- a/netbox/wireless/api/nested_serializers.py +++ b/netbox/wireless/api/nested_serializers.py @@ -4,13 +4,13 @@ from wireless.models import * __all__ = ( - 'NestedSSIDSerializer', + 'NestedWirelessLANSerializer', ) -class NestedSSIDSerializer(WritableNestedSerializer): +class NestedWirelessLANSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail') class Meta: - model = SSID - fields = ['id', 'url', 'display', 'name'] + model = WirelessLAN + fields = ['id', 'url', 'display', 'ssid'] diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index c129e5c9682..08642259fb5 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,21 +1,20 @@ from rest_framework import serializers -from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer from netbox.api.serializers import PrimaryModelSerializer from wireless.models import * __all__ = ( - 'SSIDSerializer', + 'WirelessLANSerializer', ) -class SSIDSerializer(PrimaryModelSerializer): +class WirelessLANSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail') vlan = NestedVLANSerializer(required=False, allow_null=True) class Meta: - model = SSID + model = WirelessLAN fields = [ - 'id', 'url', 'display', 'name', 'description', 'vlan', + 'id', 'url', 'display', 'ssid', 'description', 'vlan', ] diff --git a/netbox/wireless/api/urls.py b/netbox/wireless/api/urls.py index f6936708ce1..638f31bbf81 100644 --- a/netbox/wireless/api/urls.py +++ b/netbox/wireless/api/urls.py @@ -5,8 +5,7 @@ router = OrderedDefaultRouter() router.APIRootView = views.WirelessRootView -# SSIDs -router.register('ssids', views.SSIDViewSet) +router.register('wireless-lans', views.WirelessLANViewSet) app_name = 'wireless-api' urlpatterns = router.urls diff --git a/netbox/wireless/api/views.py b/netbox/wireless/api/views.py index 97827eb7eec..a09b2e23dcb 100644 --- a/netbox/wireless/api/views.py +++ b/netbox/wireless/api/views.py @@ -18,7 +18,7 @@ def get_view_name(self): # Providers # -class SSIDViewSet(CustomFieldModelViewSet): - queryset = SSID.objects.prefetch_related('tags') - serializer_class = serializers.SSIDSerializer - filterset_class = filtersets.SSIDFilterSet +class WirelessLANViewSet(CustomFieldModelViewSet): + queryset = WirelessLAN.objects.prefetch_related('tags') + serializer_class = serializers.WirelessLANSerializer + filterset_class = filtersets.WirelessLANFilterSet diff --git a/netbox/wireless/constants.py b/netbox/wireless/constants.py new file mode 100644 index 00000000000..188c4abd935 --- /dev/null +++ b/netbox/wireless/constants.py @@ -0,0 +1 @@ +SSID_MAX_LENGTH = 32 # Per IEEE 802.11-2007 diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 232bc74ff72..c148354a0aa 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -6,11 +6,11 @@ from .models import * __all__ = ( - 'SSIDFilterSet', + 'WirelessLANFilterSet', ) -class SSIDFilterSet(PrimaryModelFilterSet): +class WirelessLANFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -18,14 +18,14 @@ class SSIDFilterSet(PrimaryModelFilterSet): tag = TagFilter() class Meta: - model = SSID - fields = ['id', 'name'] + model = WirelessLAN + fields = ['id', 'ssid'] def search(self, queryset, name, value): if not value.strip(): return queryset qs_filter = ( - Q(name__icontains=value) | + Q(ssid__icontains=value) | Q(description__icontains=value) ) return queryset.filter(qs_filter) diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index ed9fb650bf7..c11a162393e 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -6,11 +6,11 @@ from utilities.forms import BootstrapMixin, DynamicModelChoiceField __all__ = ( - 'SSIDBulkEditForm', + 'WirelessLANBulkEditForm', ) -class SSIDBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerFeed.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 0cf997fd357..5dc07f91a35 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -1,14 +1,14 @@ from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN from utilities.forms import CSVModelChoiceField -from wireless.models import SSID +from wireless.models import WirelessLAN __all__ = ( - 'SSIDCSVForm', + 'WirelessLANCSVForm', ) -class SSIDCSVForm(CustomFieldModelCSVForm): +class WirelessLANCSVForm(CustomFieldModelCSVForm): vlan = CSVModelChoiceField( queryset=VLAN.objects.all(), to_field_name='name', @@ -16,5 +16,5 @@ class SSIDCSVForm(CustomFieldModelCSVForm): ) class Meta: - model = SSID - fields = ('name', 'description', 'vlan') + model = WirelessLAN + fields = ('ssid', 'description', 'vlan') diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 733b807f74b..99e38918e79 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -1,13 +1,13 @@ from django import forms from django.utils.translation import gettext as _ -from dcim.models import * from extras.forms import CustomFieldModelFilterForm from utilities.forms import BootstrapMixin, TagFilterField +from .models import WirelessLAN -class SSIDFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = PowerFeed +class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = WirelessLAN field_groups = [ ['q', 'tag'], ] diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index ea6d51223b6..95b43c7d340 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -1,17 +1,15 @@ -from dcim.constants import * -from dcim.models import * from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField -from wireless.models import SSID +from wireless.models import WirelessLAN __all__ = ( - 'SSIDForm', + 'WirelessLANForm', ) -class SSIDForm(BootstrapMixin, CustomFieldModelForm): +class WirelessLANForm(BootstrapMixin, CustomFieldModelForm): vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False @@ -22,11 +20,11 @@ class SSIDForm(BootstrapMixin, CustomFieldModelForm): ) class Meta: - model = SSID + model = WirelessLAN fields = [ - 'name', 'description', 'vlan', 'tags', + 'ssid', 'description', 'vlan', 'tags', ] fieldsets = ( - ('SSID', ('name', 'description', 'tags')), + ('Wireless LAN', ('ssid', 'description', 'tags')), ('VLAN', ('vlan',)), ) diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py index d0beec7d9f7..8297f454516 100644 --- a/netbox/wireless/graphql/schema.py +++ b/netbox/wireless/graphql/schema.py @@ -5,5 +5,5 @@ class WirelessQuery(graphene.ObjectType): - ssid = ObjectField(SSIDType) - ssid_list = ObjectListField(SSIDType) + wirelesslan = ObjectField(WirelessLANType) + wirelesslan_list = ObjectListField(WirelessLANType) diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 66e73429ddc..4cdb75ebe33 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -2,13 +2,13 @@ from netbox.graphql.types import ObjectType __all__ = ( - 'SSIDType', + 'WirelessLANType', ) -class SSIDType(ObjectType): +class WirelessLANType(ObjectType): class Meta: - model = models.SSID + model = models.WirelessLAN fields = '__all__' - filterset_class = filtersets.SSIDFilterSet + filterset_class = filtersets.WirelessLANFilterSet diff --git a/netbox/wireless/migrations/0001_initial.py b/netbox/wireless/migrations/0001_initial.py index 78d1dfc7379..c93a1719099 100644 --- a/netbox/wireless/migrations/0001_initial.py +++ b/netbox/wireless/migrations/0001_initial.py @@ -9,27 +9,26 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('extras', '0062_clear_secrets_changelog'), ('ipam', '0050_iprange'), + ('extras', '0062_clear_secrets_changelog'), ] operations = [ migrations.CreateModel( - name='SSID', + name='WirelessLAN', fields=[ ('created', models.DateField(auto_now_add=True, null=True)), ('last_updated', models.DateTimeField(auto_now=True, null=True)), ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=32)), + ('ssid', models.CharField(max_length=32)), ('description', models.CharField(blank=True, max_length=200)), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')), ], options={ - 'verbose_name': 'SSID', - 'verbose_name_plural': 'SSIDs', - 'ordering': ('name', 'pk'), + 'verbose_name': 'Wireless LAN', + 'ordering': ('ssid', 'pk'), }, ), ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 2bdcecd79af..363631ef540 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -1,25 +1,23 @@ +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse +from dcim.constants import WIRELESS_IFACE_TYPES from extras.utils import extras_features from netbox.models import BigIDModel, PrimaryModel from utilities.querysets import RestrictedQuerySet __all__ = ( - 'SSID', + 'WirelessLAN', ) -# -# SSIDs -# - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class SSID(PrimaryModel): +class WirelessLAN(PrimaryModel): """ A service set identifier belonging to a wireless network. """ - name = models.CharField( + ssid = models.CharField( max_length=32 ) vlan = models.ForeignKey( @@ -37,12 +35,11 @@ class SSID(PrimaryModel): objects = RestrictedQuerySet.as_manager() class Meta: - ordering = ('name', 'pk') - verbose_name = 'SSID' - verbose_name_plural = 'SSIDs' + ordering = ('ssid', 'pk') + verbose_name = 'Wireless LAN' def __str__(self): - return self.name + return self.ssid def get_absolute_url(self): return reverse('wireless:ssid', args=[self.pk]) diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 9d37055495e..133353f5743 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -1,23 +1,23 @@ import django_tables2 as tables -from .models import SSID +from .models import WirelessLAN from utilities.tables import BaseTable, TagColumn, ToggleColumn __all__ = ( - 'SSIDTable', + 'WirelessLANTable', ) -class SSIDTable(BaseTable): +class WirelessLANTable(BaseTable): pk = ToggleColumn() - name = tables.Column( + ssid = tables.Column( linkify=True ) tags = TagColumn( - url_name='dcim:cable_list' + url_name='wireless:wirelesslan_list' ) class Meta(BaseTable.Meta): - model = SSID - fields = ('pk', 'name', 'description', 'vlan') - default_columns = ('pk', 'name', 'description', 'vlan') + model = WirelessLAN + fields = ('pk', 'ssid', 'description', 'vlan') + default_columns = ('pk', 'ssid', 'description', 'vlan') diff --git a/netbox/wireless/urls.py b/netbox/wireless/urls.py index 57e0eab9b1f..c30ca472c79 100644 --- a/netbox/wireless/urls.py +++ b/netbox/wireless/urls.py @@ -7,16 +7,16 @@ app_name = 'wireless' urlpatterns = ( - # SSIDs - path('ssids/', views.SSIDListView.as_view(), name='ssid_list'), - path('ssids/add/', views.SSIDEditView.as_view(), name='ssid_add'), - path('ssids/import/', views.SSIDBulkImportView.as_view(), name='ssid_import'), - path('ssids/edit/', views.SSIDBulkEditView.as_view(), name='ssid_bulk_edit'), - path('ssids/delete/', views.SSIDBulkDeleteView.as_view(), name='ssid_bulk_delete'), - path('ssids//', views.SSIDView.as_view(), name='ssid'), - path('ssids//edit/', views.SSIDEditView.as_view(), name='ssid_edit'), - path('ssids//delete/', views.SSIDDeleteView.as_view(), name='ssid_delete'), - path('ssids//changelog/', ObjectChangeLogView.as_view(), name='ssid_changelog', kwargs={'model': SSID}), - path('ssids//journal/', ObjectJournalView.as_view(), name='ssid_journal', kwargs={'model': SSID}), + # Wireless LANs + path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'), + path('wireless-lans/add/', views.WirelessLANEditView.as_view(), name='wirelesslan_add'), + path('wireless-lans/import/', views.WirelessLANBulkImportView.as_view(), name='wirelesslan_import'), + path('wireless-lans/edit/', views.WirelessLANBulkEditView.as_view(), name='wirelesslan_bulk_edit'), + path('wireless-lans/delete/', views.WirelessLANBulkDeleteView.as_view(), name='wirelesslan_bulk_delete'), + path('wireless-lans//', views.WirelessLANView.as_view(), name='wirelesslan'), + path('wireless-lans//edit/', views.WirelessLANEditView.as_view(), name='wirelesslan_edit'), + path('wireless-lans//delete/', views.WirelessLANDeleteView.as_view(), name='wirelesslan_delete'), + path('wireless-lans//changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}), + path('wireless-lans//journal/', ObjectJournalView.as_view(), name='wirelesslan_journal', kwargs={'model': WirelessLAN}), ) diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index b741330b73d..6e1c0b1b7b9 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -4,43 +4,43 @@ # -# SSIDs +# Wireless LANs # -class SSIDListView(generic.ObjectListView): - queryset = SSID.objects.all() - filterset = filtersets.SSIDFilterSet - filterset_form = forms.SSIDFilterForm - table = tables.SSIDTable +class WirelessLANListView(generic.ObjectListView): + queryset = WirelessLAN.objects.all() + filterset = filtersets.WirelessLANFilterSet + filterset_form = forms.WirelessLANFilterForm + table = tables.WirelessLANTable -class SSIDView(generic.ObjectView): - queryset = SSID.objects.all() +class WirelessLANView(generic.ObjectView): + queryset = WirelessLAN.objects.all() -class SSIDEditView(generic.ObjectEditView): - queryset = SSID.objects.all() - model_form = forms.SSIDForm +class WirelessLANEditView(generic.ObjectEditView): + queryset = WirelessLAN.objects.all() + model_form = forms.WirelessLANForm -class SSIDDeleteView(generic.ObjectDeleteView): - queryset = SSID.objects.all() +class WirelessLANDeleteView(generic.ObjectDeleteView): + queryset = WirelessLAN.objects.all() -class SSIDBulkImportView(generic.BulkImportView): - queryset = SSID.objects.all() - model_form = forms.SSIDCSVForm - table = tables.SSIDTable +class WirelessLANBulkImportView(generic.BulkImportView): + queryset = WirelessLAN.objects.all() + model_form = forms.WirelessLANCSVForm + table = tables.WirelessLANTable -class SSIDBulkEditView(generic.BulkEditView): - queryset = SSID.objects.all() - filterset = filtersets.SSIDFilterSet - table = tables.SSIDTable - form = forms.SSIDBulkEditForm +class WirelessLANBulkEditView(generic.BulkEditView): + queryset = WirelessLAN.objects.all() + filterset = filtersets.WirelessLANFilterSet + table = tables.WirelessLANTable + form = forms.WirelessLANBulkEditForm -class SSIDBulkDeleteView(generic.BulkDeleteView): - queryset = SSID.objects.all() - filterset = filtersets.SSIDFilterSet - table = tables.SSIDTable +class WirelessLANBulkDeleteView(generic.BulkDeleteView): + queryset = WirelessLAN.objects.all() + filterset = filtersets.WirelessLANFilterSet + table = tables.WirelessLANTable From 90e9f344944ef192b4b3ebc56a895d93e2995440 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 09:46:17 -0400 Subject: [PATCH 05/28] Add WirelessLink model --- netbox/dcim/migrations/0136_wireless.py | 8 +- netbox/dcim/migrations/0137_wireless.py | 19 +++++ netbox/netbox/navigation_menu.py | 1 + .../wireless/inc/wirelesslink_interface.html | 20 +++++ netbox/templates/wireless/wirelesslan.html | 2 +- netbox/templates/wireless/wirelesslink.html | 48 +++++++++++ netbox/wireless/api/nested_serializers.py | 11 ++- netbox/wireless/api/serializers.py | 17 +++- netbox/wireless/api/urls.py | 1 + netbox/wireless/api/views.py | 12 +-- netbox/wireless/filtersets.py | 22 +++++ netbox/wireless/forms/bulk_edit.py | 27 +++++- netbox/wireless/forms/bulk_import.py | 17 +++- netbox/wireless/forms/filtersets.py | 28 ++++++- netbox/wireless/forms/models.py | 29 ++++++- netbox/wireless/migrations/0001_initial.py | 34 -------- netbox/wireless/migrations/0001_wireless.py | 57 +++++++++++++ netbox/wireless/models.py | 83 ++++++++++++++++++- netbox/wireless/tables.py | 25 +++++- netbox/wireless/urls.py | 12 +++ netbox/wireless/views.py | 43 ++++++++++ 21 files changed, 458 insertions(+), 58 deletions(-) create mode 100644 netbox/dcim/migrations/0137_wireless.py create mode 100644 netbox/templates/wireless/inc/wirelesslink_interface.html create mode 100644 netbox/templates/wireless/wirelesslink.html delete mode 100644 netbox/wireless/migrations/0001_initial.py create mode 100644 netbox/wireless/migrations/0001_wireless.py diff --git a/netbox/dcim/migrations/0136_wireless.py b/netbox/dcim/migrations/0136_wireless.py index 108e638028d..3b33f7d3f0c 100644 --- a/netbox/dcim/migrations/0136_wireless.py +++ b/netbox/dcim/migrations/0136_wireless.py @@ -1,10 +1,11 @@ +# Generated by Django 3.2.8 on 2021-10-13 13:44 + from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('wireless', '0001_initial'), ('dcim', '0135_location_tenant'), ] @@ -19,9 +20,4 @@ class Migration(migrations.Migration): name='rf_channel_width', field=models.PositiveSmallIntegerField(blank=True, null=True), ), - migrations.AddField( - model_name='interface', - name='wireless_lans', - field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.WirelessLAN'), - ), ] diff --git a/netbox/dcim/migrations/0137_wireless.py b/netbox/dcim/migrations/0137_wireless.py new file mode 100644 index 00000000000..9108735a1a5 --- /dev/null +++ b/netbox/dcim/migrations/0137_wireless.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.8 on 2021-10-13 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0136_wireless'), + ('wireless', '0001_wireless'), + ] + + operations = [ + migrations.AddField( + model_name='interface', + name='wireless_lans', + field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.WirelessLAN'), + ), + ] diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation_menu.py index 073189d31ae..b3e11f6cea3 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation_menu.py @@ -196,6 +196,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): label='Wireless', items=( get_model_item('wireless', 'wirelesslan', 'Wireless LANs'), + get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']), ), ), ), diff --git a/netbox/templates/wireless/inc/wirelesslink_interface.html b/netbox/templates/wireless/inc/wirelesslink_interface.html new file mode 100644 index 00000000000..9c4669ad1c7 --- /dev/null +++ b/netbox/templates/wireless/inc/wirelesslink_interface.html @@ -0,0 +1,20 @@ +
Name{{ object.name }}SSID{{ object.ssid }}
Description
+ + + + + + + + + + + + +
Device + {{ interface.device }} +
Interface + {{ interface }} +
Type + {{ interface.get_type_display }} +
diff --git a/netbox/templates/wireless/wirelesslan.html b/netbox/templates/wireless/wirelesslan.html index 98bde8688d9..f8fabf558eb 100644 --- a/netbox/templates/wireless/wirelesslan.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -30,7 +30,7 @@
Wireless LAN
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %} + {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='wireless:wirelesslan_list' %} {% plugin_left_page object %}
diff --git a/netbox/templates/wireless/wirelesslink.html b/netbox/templates/wireless/wirelesslink.html new file mode 100644 index 00000000000..6196adae4e4 --- /dev/null +++ b/netbox/templates/wireless/wirelesslink.html @@ -0,0 +1,48 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} + +{% block content %} +
+
+
+
Interface A
+
+ {% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_a %} +
+
+
+
Link Properties
+
+ + + + + + + + + +
SSID{{ object.ssid|placeholder }}
Description{{ object.description|placeholder }}
+
+
+ {% plugin_left_page object %} +
+
+
+
Interface B
+
+ {% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_b %} +
+
+ {% include 'inc/custom_fields_panel.html' %} + {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='wireless:wirelesslink_list' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox/wireless/api/nested_serializers.py b/netbox/wireless/api/nested_serializers.py index e290653a2a6..5a8cf66716d 100644 --- a/netbox/wireless/api/nested_serializers.py +++ b/netbox/wireless/api/nested_serializers.py @@ -5,12 +5,21 @@ __all__ = ( 'NestedWirelessLANSerializer', + 'NestedWirelessLinkSerializer', ) class NestedWirelessLANSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail') + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') class Meta: model = WirelessLAN fields = ['id', 'url', 'display', 'ssid'] + + +class NestedWirelessLinkSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') + + class Meta: + model = WirelessLink + fields = ['id', 'url', 'display', 'ssid'] diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 08642259fb5..90515f53e8e 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,16 +1,19 @@ from rest_framework import serializers +from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer from netbox.api.serializers import PrimaryModelSerializer from wireless.models import * +from .nested_serializers import * __all__ = ( 'WirelessLANSerializer', + 'WirelessLinkSerializer', ) class WirelessLANSerializer(PrimaryModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail') + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') vlan = NestedVLANSerializer(required=False, allow_null=True) class Meta: @@ -18,3 +21,15 @@ class Meta: fields = [ 'id', 'url', 'display', 'ssid', 'description', 'vlan', ] + + +class WirelessLinkSerializer(PrimaryModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') + interface_a = NestedInterfaceSerializer() + interface_b = NestedInterfaceSerializer() + + class Meta: + model = WirelessLAN + fields = [ + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'description', + ] diff --git a/netbox/wireless/api/urls.py b/netbox/wireless/api/urls.py index 638f31bbf81..431bb05f812 100644 --- a/netbox/wireless/api/urls.py +++ b/netbox/wireless/api/urls.py @@ -6,6 +6,7 @@ router.APIRootView = views.WirelessRootView router.register('wireless-lans', views.WirelessLANViewSet) +router.register('wireless-links', views.WirelessLinkViewSet) app_name = 'wireless-api' urlpatterns = router.urls diff --git a/netbox/wireless/api/views.py b/netbox/wireless/api/views.py index a09b2e23dcb..aa361a7f765 100644 --- a/netbox/wireless/api/views.py +++ b/netbox/wireless/api/views.py @@ -14,11 +14,13 @@ def get_view_name(self): return 'Wireless' -# -# Providers -# - class WirelessLANViewSet(CustomFieldModelViewSet): - queryset = WirelessLAN.objects.prefetch_related('tags') + queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags') serializer_class = serializers.WirelessLANSerializer filterset_class = filtersets.WirelessLANFilterSet + + +class WirelessLinkViewSet(CustomFieldModelViewSet): + queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags') + serializer_class = serializers.WirelessLinkSerializer + filterset_class = filtersets.WirelessLinkFilterSet diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index c148354a0aa..7341ada9df7 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -7,6 +7,7 @@ __all__ = ( 'WirelessLANFilterSet', + 'WirelessLinkFilterSet', ) @@ -29,3 +30,24 @@ def search(self, queryset, name, value): Q(description__icontains=value) ) return queryset.filter(qs_filter) + + +class WirelessLinkFilterSet(PrimaryModelFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + tag = TagFilter() + + class Meta: + model = WirelessLink + fields = ['id', 'ssid'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = ( + Q(ssid__icontains=value) | + Q(description__icontains=value) + ) + return queryset.filter(qs_filter) diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index c11a162393e..65666ccb1c6 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -4,9 +4,11 @@ from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import VLAN from utilities.forms import BootstrapMixin, DynamicModelChoiceField +from wireless.constants import SSID_MAX_LENGTH __all__ = ( 'WirelessLANBulkEditForm', + 'WirelessLinkBulkEditForm', ) @@ -19,11 +21,30 @@ class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode queryset=VLAN.objects.all(), required=False, ) + ssid = forms.CharField( + max_length=SSID_MAX_LENGTH, + required=False + ) + description = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ['vlan', 'ssid', 'description'] + + +class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerFeed.objects.all(), + widget=forms.MultipleHiddenInput + ) + ssid = forms.CharField( + max_length=SSID_MAX_LENGTH, + required=False + ) description = forms.CharField( required=False ) class Meta: - nullable_fields = [ - 'vlan', 'description', - ] + nullable_fields = ['ssid', 'description'] diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 5dc07f91a35..caf322dc1b7 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -1,10 +1,12 @@ +from dcim.models import Interface from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN from utilities.forms import CSVModelChoiceField -from wireless.models import WirelessLAN +from wireless.models import * __all__ = ( 'WirelessLANCSVForm', + 'WirelessLinkCSVForm', ) @@ -18,3 +20,16 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm): class Meta: model = WirelessLAN fields = ('ssid', 'description', 'vlan') + + +class WirelessLinkCSVForm(CustomFieldModelCSVForm): + interface_a = CSVModelChoiceField( + queryset=Interface.objects.all() + ) + interface_b = CSVModelChoiceField( + queryset=Interface.objects.all() + ) + + class Meta: + model = WirelessLink + fields = ('interface_a', 'interface_b', 'ssid', 'description') diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 99e38918e79..fa191209991 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -3,7 +3,12 @@ from extras.forms import CustomFieldModelFilterForm from utilities.forms import BootstrapMixin, TagFilterField -from .models import WirelessLAN +from wireless.models import * + +__all__ = ( + 'WirelessLANFilterForm', + 'WirelessLinkFilterForm', +) class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm): @@ -16,4 +21,25 @@ class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm): widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), label=_('Search') ) + ssid = forms.CharField( + required=False, + label='SSID' + ) + tag = TagFilterField(model) + + +class WirelessLinkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = WirelessLink + field_groups = [ + ['q', 'tag'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + ssid = forms.CharField( + required=False, + label='SSID' + ) tag = TagFilterField(model) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 95b43c7d340..08e864340fa 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -1,11 +1,13 @@ +from dcim.models import Interface from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField -from wireless.models import WirelessLAN +from wireless.models import * __all__ = ( 'WirelessLANForm', + 'WirelessLinkForm', ) @@ -28,3 +30,28 @@ class Meta: ('Wireless LAN', ('ssid', 'description', 'tags')), ('VLAN', ('vlan',)), ) + + +class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): + interface_a = DynamicModelChoiceField( + queryset=Interface.objects.all(), + query_params={ + 'kind': 'wireless' + } + ) + interface_b = DynamicModelChoiceField( + queryset=Interface.objects.all(), + query_params={ + 'kind': 'wireless' + } + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + class Meta: + model = WirelessLink + fields = [ + 'interface_a', 'interface_b', 'ssid', 'description', 'tags', + ] diff --git a/netbox/wireless/migrations/0001_initial.py b/netbox/wireless/migrations/0001_initial.py deleted file mode 100644 index c93a1719099..00000000000 --- a/netbox/wireless/migrations/0001_initial.py +++ /dev/null @@ -1,34 +0,0 @@ -import django.core.serializers.json -from django.db import migrations, models -import django.db.models.deletion -import taggit.managers - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('ipam', '0050_iprange'), - ('extras', '0062_clear_secrets_changelog'), - ] - - operations = [ - migrations.CreateModel( - name='WirelessLAN', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('ssid', models.CharField(max_length=32)), - ('description', models.CharField(blank=True, max_length=200)), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), - ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')), - ], - options={ - 'verbose_name': 'Wireless LAN', - 'ordering': ('ssid', 'pk'), - }, - ), - ] diff --git a/netbox/wireless/migrations/0001_wireless.py b/netbox/wireless/migrations/0001_wireless.py new file mode 100644 index 00000000000..2fb07e5fde2 --- /dev/null +++ b/netbox/wireless/migrations/0001_wireless.py @@ -0,0 +1,57 @@ +# Generated by Django 3.2.8 on 2021-10-13 13:44 + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('dcim', '0136_wireless'), + ('extras', '0062_clear_secrets_changelog'), + ('ipam', '0050_iprange'), + ] + + operations = [ + migrations.CreateModel( + name='WirelessLAN', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('ssid', models.CharField(max_length=32)), + ('description', models.CharField(blank=True, max_length=200)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')), + ], + options={ + 'verbose_name': 'Wireless LAN', + 'ordering': ('ssid', 'pk'), + }, + ), + migrations.CreateModel( + name='WirelessLink', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('ssid', models.CharField(blank=True, max_length=32)), + ('description', models.CharField(blank=True, max_length=200)), + ('_interface_a_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')), + ('_interface_b_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')), + ('interface_a', models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface')), + ('interface_b', models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ['pk'], + 'unique_together': {('interface_a', 'interface_b')}, + }, + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 363631ef540..41f3dbf6da8 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -6,19 +6,22 @@ from extras.utils import extras_features from netbox.models import BigIDModel, PrimaryModel from utilities.querysets import RestrictedQuerySet +from .constants import SSID_MAX_LENGTH __all__ = ( 'WirelessLAN', + 'WirelessLink', ) @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class WirelessLAN(PrimaryModel): """ - A service set identifier belonging to a wireless network. + A wireless network formed among an arbitrary number of access point and clients. """ ssid = models.CharField( - max_length=32 + max_length=SSID_MAX_LENGTH, + verbose_name='SSID' ) vlan = models.ForeignKey( to='ipam.VLAN', @@ -42,4 +45,78 @@ def __str__(self): return self.ssid def get_absolute_url(self): - return reverse('wireless:ssid', args=[self.pk]) + return reverse('wireless:wirelesslan', args=[self.pk]) + + +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') +class WirelessLink(PrimaryModel): + """ + A point-to-point connection between two wireless Interfaces. + """ + interface_a = models.ForeignKey( + to='dcim.Interface', + limit_choices_to={'type__in': WIRELESS_IFACE_TYPES}, + on_delete=models.PROTECT, + related_name='+' + ) + interface_b = models.ForeignKey( + to='dcim.Interface', + limit_choices_to={'type__in': WIRELESS_IFACE_TYPES}, + on_delete=models.PROTECT, + related_name='+' + ) + ssid = models.CharField( + max_length=SSID_MAX_LENGTH, + blank=True, + verbose_name='SSID' + ) + description = models.CharField( + max_length=200, + blank=True + ) + + # Cache the associated device for the A and B interfaces. This enables filtering of WirelessLinks by their + # associated Devices. + _interface_a_device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='+', + blank=True, + null=True + ) + _interface_b_device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='+', + blank=True, + null=True + ) + + objects = RestrictedQuerySet.as_manager() + + class Meta: + ordering = ['pk'] + unique_together = ('interface_a', 'interface_b') + + def get_absolute_url(self): + return reverse('wireless:wirelesslink', args=[self.pk]) + + def clean(self): + + # Validate interface types + if self.interface_a.type not in WIRELESS_IFACE_TYPES: + raise ValidationError({ + 'interface_a': f"{self.interface_a.get_type_display()} is not a wireless interface." + }) + if self.interface_b.type not in WIRELESS_IFACE_TYPES: + raise ValidationError({ + 'interface_a': f"{self.interface_b.get_type_display()} is not a wireless interface." + }) + + def save(self, *args, **kwargs): + + # Store the parent Device for the A and B interfaces + self._interface_a_device = self.interface_a.device + self._interface_b_device = self.interface_b.device + + super().save(*args, **kwargs) diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 133353f5743..31c9e56a84d 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -1,10 +1,11 @@ import django_tables2 as tables -from .models import WirelessLAN +from .models import * from utilities.tables import BaseTable, TagColumn, ToggleColumn __all__ = ( 'WirelessLANTable', + 'WirelessLinkTable', ) @@ -21,3 +22,25 @@ class Meta(BaseTable.Meta): model = WirelessLAN fields = ('pk', 'ssid', 'description', 'vlan') default_columns = ('pk', 'ssid', 'description', 'vlan') + + +class WirelessLinkTable(BaseTable): + pk = ToggleColumn() + id = tables.Column( + linkify=True, + verbose_name='ID' + ) + interface_a = tables.Column( + linkify=True + ) + interface_b = tables.Column( + linkify=True + ) + tags = TagColumn( + url_name='wireless:wirelesslink_list' + ) + + class Meta(BaseTable.Meta): + model = WirelessLink + fields = ('pk', 'id', 'interface_a', 'interface_b', 'ssid', 'description') + default_columns = ('pk', 'id', 'interface_a', 'interface_b', 'ssid', 'description') diff --git a/netbox/wireless/urls.py b/netbox/wireless/urls.py index c30ca472c79..21d704e6a7c 100644 --- a/netbox/wireless/urls.py +++ b/netbox/wireless/urls.py @@ -19,4 +19,16 @@ path('wireless-lans//changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}), path('wireless-lans//journal/', ObjectJournalView.as_view(), name='wirelesslan_journal', kwargs={'model': WirelessLAN}), + # Wireless links + path('wireless-links/', views.WirelessLinkListView.as_view(), name='wirelesslink_list'), + path('wireless-links/add/', views.WirelessLinkEditView.as_view(), name='wirelesslink_add'), + path('wireless-links/import/', views.WirelessLinkBulkImportView.as_view(), name='wirelesslink_import'), + path('wireless-links/edit/', views.WirelessLinkBulkEditView.as_view(), name='wirelesslink_bulk_edit'), + path('wireless-links/delete/', views.WirelessLinkBulkDeleteView.as_view(), name='wirelesslink_bulk_delete'), + path('wireless-links//', views.WirelessLinkView.as_view(), name='wirelesslink'), + path('wireless-links//edit/', views.WirelessLinkEditView.as_view(), name='wirelesslink_edit'), + path('wireless-links//delete/', views.WirelessLinkDeleteView.as_view(), name='wirelesslink_delete'), + path('wireless-links//changelog/', ObjectChangeLogView.as_view(), name='wirelesslink_changelog', kwargs={'model': WirelessLink}), + path('wireless-links//journal/', ObjectJournalView.as_view(), name='wirelesslink_journal', kwargs={'model': WirelessLink}), + ) diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index 6e1c0b1b7b9..041ffbd420f 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -44,3 +44,46 @@ class WirelessLANBulkDeleteView(generic.BulkDeleteView): queryset = WirelessLAN.objects.all() filterset = filtersets.WirelessLANFilterSet table = tables.WirelessLANTable + + +# +# Wireless Links +# + +class WirelessLinkListView(generic.ObjectListView): + queryset = WirelessLink.objects.all() + filterset = filtersets.WirelessLinkFilterSet + filterset_form = forms.WirelessLinkFilterForm + table = tables.WirelessLinkTable + + +class WirelessLinkView(generic.ObjectView): + queryset = WirelessLink.objects.all() + + +class WirelessLinkEditView(generic.ObjectEditView): + queryset = WirelessLink.objects.all() + model_form = forms.WirelessLinkForm + + +class WirelessLinkDeleteView(generic.ObjectDeleteView): + queryset = WirelessLink.objects.all() + + +class WirelessLinkBulkImportView(generic.BulkImportView): + queryset = WirelessLink.objects.all() + model_form = forms.WirelessLinkCSVForm + table = tables.WirelessLinkTable + + +class WirelessLinkBulkEditView(generic.BulkEditView): + queryset = WirelessLink.objects.all() + filterset = filtersets.WirelessLinkFilterSet + table = tables.WirelessLinkTable + form = forms.WirelessLinkBulkEditForm + + +class WirelessLinkBulkDeleteView(generic.BulkDeleteView): + queryset = WirelessLink.objects.all() + filterset = filtersets.WirelessLinkFilterSet + table = tables.WirelessLinkTable From 445e16f6682e250f855c2cc5185e8dce4f146e51 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 11:48:15 -0400 Subject: [PATCH 06/28] Reference WirelessLink on both attached Interfaces --- .../0138_interface_wireless_link.py | 20 +++++++ netbox/dcim/models/device_components.py | 19 ++++-- netbox/dcim/tables/template_code.py | 2 +- netbox/templates/dcim/interface.html | 19 +++++- netbox/wireless/apps.py | 3 + netbox/wireless/forms/models.py | 6 +- netbox/wireless/signals.py | 58 +++++++++++++++++++ 7 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 netbox/dcim/migrations/0138_interface_wireless_link.py create mode 100644 netbox/wireless/signals.py diff --git a/netbox/dcim/migrations/0138_interface_wireless_link.py b/netbox/dcim/migrations/0138_interface_wireless_link.py new file mode 100644 index 00000000000..42b7a10422a --- /dev/null +++ b/netbox/dcim/migrations/0138_interface_wireless_link.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.8 on 2021-10-13 15:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wireless', '0001_wireless'), + ('dcim', '0137_wireless'), + ] + + operations = [ + migrations.AddField( + model_name='interface', + name='wireless_link', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wireless.wirelesslink'), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 60eb4c368f1..dc82469901b 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -529,6 +529,13 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): null=True, verbose_name='Channel width' ) + wireless_link = models.ForeignKey( + to='wireless.WirelessLink', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) wireless_lans = models.ManyToManyField( to='wireless.WirelessLAN', related_name='interfaces', @@ -568,14 +575,14 @@ def get_absolute_url(self): def clean(self): super().clean() - # Virtual interfaces cannot be connected - if not self.is_connectable and self.cable: + # Virtual Interfaces cannot have a Cable attached + if self.is_virtual and self.cable: raise ValidationError({ 'type': f"{self.get_type_display()} interfaces cannot have a cable attached." }) - # Non-connectable interfaces cannot be marked as connected - if not self.is_connectable and self.mark_connected: + # Virtual Interfaces cannot be marked as connected + if self.is_virtual and self.mark_connected: raise ValidationError({ 'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected." }) @@ -635,8 +642,8 @@ def clean(self): }) @property - def is_connectable(self): - return self.type not in NONCONNECTABLE_IFACE_TYPES + def is_wired(self): + return not self.is_virtual and not self.is_wireless @property def is_virtual(self): diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 2f359e1b95e..9e2dc519bfd 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -203,7 +203,7 @@ {% endif %} -{% elif record.is_connectable and perms.dcim.add_cable %} +{% elif record.is_wired and perms.dcim.add_cable %} {% if not record.mark_connected %} diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 33eaa95dbb4..bf24a89ef02 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -117,7 +117,7 @@
{% plugin_left_page object %}
- {% if object.is_connectable %} + {% if not object.is_virtual %}
Connection @@ -221,10 +221,19 @@
+ {% elif object.wireless_link %} + + + + + +
Wireless Link + {{ object.wireless_link }} +
{% else %}
Not Connected - {% if perms.dcim.add_cable %} + {% if object.is_wired and perms.dcim.add_cable %} + {% elif object.is_wireless and perms.wireless.add_wirelesslink %} + {% endif %}
{% endif %} diff --git a/netbox/wireless/apps.py b/netbox/wireless/apps.py index 1f6deff2298..59e47aba54b 100644 --- a/netbox/wireless/apps.py +++ b/netbox/wireless/apps.py @@ -3,3 +3,6 @@ class WirelessConfig(AppConfig): name = 'wireless' + + def ready(self): + import wireless.signals diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 08e864340fa..c494fb5a266 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -37,13 +37,15 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): queryset=Interface.objects.all(), query_params={ 'kind': 'wireless' - } + }, + label='Interface A' ) interface_b = DynamicModelChoiceField( queryset=Interface.objects.all(), query_params={ 'kind': 'wireless' - } + }, + label='Interface B' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), diff --git a/netbox/wireless/signals.py b/netbox/wireless/signals.py new file mode 100644 index 00000000000..a42566a000d --- /dev/null +++ b/netbox/wireless/signals.py @@ -0,0 +1,58 @@ +import logging + +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from dcim.models import Interface +from .models import WirelessLink + + +# +# Wireless links +# + +@receiver(post_save, sender=WirelessLink) +def update_connected_interfaces(instance, raw=False, **kwargs): + """ + When a WirelessLink is saved, save a reference to it on each connected interface. + """ + print('update_connected_interfaces') + logger = logging.getLogger('netbox.wireless.wirelesslink') + if raw: + logger.debug(f"Skipping endpoint updates for imported wireless link {instance}") + return + + if instance.interface_a.wireless_link != instance: + logger.debug(f"Updating interface A for wireless link {instance}") + instance.interface_a.wireless_link = instance + # instance.interface_a._cable_peer = instance.interface_b # TODO: Rename _cable_peer field + instance.interface_a.save() + if instance.interface_b.cable != instance: + logger.debug(f"Updating interface B for wireless link {instance}") + instance.interface_b.wireless_link = instance + # instance.interface_b._cable_peer = instance.interface_a + instance.interface_b.save() + + +@receiver(post_delete, sender=WirelessLink) +def nullify_connected_interfaces(instance, **kwargs): + """ + When a WirelessLink is deleted, update its two connected Interfaces + """ + print('nullify_connected_interfaces') + logger = logging.getLogger('netbox.wireless.wirelesslink') + + if instance.interface_a is not None: + logger.debug(f"Nullifying interface A for wireless link {instance}") + Interface.objects.filter(pk=instance.interface_a.pk).update( + wireless_link=None, + _cable_peer_type=None, + _cable_peer_id=None + ) + if instance.interface_b is not None: + logger.debug(f"Nullifying interface B for wireless link {instance}") + Interface.objects.filter(pk=instance.interface_b.pk).update( + wireless_link=None, + _cable_peer_type=None, + _cable_peer_id=None + ) From 138af27bf7c18d6833233ff84029eb8ef6a186bf Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 13:28:14 -0400 Subject: [PATCH 07/28] Record wireless links as part of cable path --- netbox/dcim/models/cables.py | 10 ++++----- netbox/dcim/models/device_components.py | 11 ++++++++++ netbox/dcim/signals.py | 28 +------------------------ netbox/dcim/utils.py | 27 ++++++++++++++++++++++++ netbox/wireless/signals.py | 21 +++++++++++++------ 5 files changed, 59 insertions(+), 38 deletions(-) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index c3f8cac3feb..6c61c071292 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -379,7 +379,7 @@ def from_origin(cls, origin): """ from circuits.models import CircuitTermination - if origin is None or origin.cable is None: + if origin is None or origin.link is None: return None destination = None @@ -389,12 +389,12 @@ def from_origin(cls, origin): is_split = False node = origin - while node.cable is not None: - if node.cable.status != CableStatusChoices.STATUS_CONNECTED: + while node.link is not None: + if hasattr(node.link, 'status') and node.link.status != CableStatusChoices.STATUS_CONNECTED: is_active = False - # Follow the cable to its far-end termination - path.append(object_to_path_node(node.cable)) + # Follow the link to its far-end termination + path.append(object_to_path_node(node.link)) peer_termination = node.get_cable_peer() # Follow a FrontPort to its corresponding RearPort diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index dc82469901b..38e902f22b0 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -157,6 +157,13 @@ def _occupied(self): def parent_object(self): raise NotImplementedError("CableTermination models must implement parent_object()") + @property + def link(self): + """ + Generic wrapper for a Cable, WirelessLink, or some other relation to a connected termination. + """ + return self.cable + class PathEndpoint(models.Model): """ @@ -657,6 +664,10 @@ def is_wireless(self): def is_lag(self): return self.type == InterfaceTypeChoices.TYPE_LAG + @property + def link(self): + return self.cable or self.wireless_link + # # Pass-through ports diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 9fc68ee70f5..942bf04e4f1 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -2,37 +2,11 @@ from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_save, post_delete, pre_delete -from django.db import transaction from django.dispatch import receiver from .choices import CableStatusChoices from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis - - -def create_cablepath(node): - """ - Create CablePaths for all paths originating from the specified node. - """ - cp = CablePath.from_origin(node) - if cp: - try: - cp.save() - except Exception as e: - print(node, node.pk) - raise e - - -def rebuild_paths(obj): - """ - Rebuild all CablePaths which traverse the specified node - """ - cable_paths = CablePath.objects.filter(path__contains=obj) - - with transaction.atomic(): - for cp in cable_paths: - cp.delete() - if cp.origin: - create_cablepath(cp.origin) +from .utils import create_cablepath, rebuild_paths # diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 91c5c7c772d..ec3a4460370 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from django.db import transaction def compile_path_node(ct_id, object_id): @@ -26,3 +27,29 @@ def path_node_to_object(repr): ct_id, object_id = decompile_path_node(repr) ct = ContentType.objects.get_for_id(ct_id) return ct.model_class().objects.get(pk=object_id) + + +def create_cablepath(node): + """ + Create CablePaths for all paths originating from the specified node. + """ + from dcim.models import CablePath + + cp = CablePath.from_origin(node) + if cp: + cp.save() + + +def rebuild_paths(obj): + """ + Rebuild all CablePaths which traverse the specified node + """ + from dcim.models import CablePath + + cable_paths = CablePath.objects.filter(path__contains=obj) + + with transaction.atomic(): + for cp in cable_paths: + cp.delete() + if cp.origin: + create_cablepath(cp.origin) diff --git a/netbox/wireless/signals.py b/netbox/wireless/signals.py index a42566a000d..b8dc8a18660 100644 --- a/netbox/wireless/signals.py +++ b/netbox/wireless/signals.py @@ -3,7 +3,8 @@ from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from dcim.models import Interface +from dcim.models import CablePath, Interface +from dcim.utils import create_cablepath from .models import WirelessLink @@ -12,11 +13,10 @@ # @receiver(post_save, sender=WirelessLink) -def update_connected_interfaces(instance, raw=False, **kwargs): +def update_connected_interfaces(instance, created, raw=False, **kwargs): """ When a WirelessLink is saved, save a reference to it on each connected interface. """ - print('update_connected_interfaces') logger = logging.getLogger('netbox.wireless.wirelesslink') if raw: logger.debug(f"Skipping endpoint updates for imported wireless link {instance}") @@ -25,21 +25,25 @@ def update_connected_interfaces(instance, raw=False, **kwargs): if instance.interface_a.wireless_link != instance: logger.debug(f"Updating interface A for wireless link {instance}") instance.interface_a.wireless_link = instance - # instance.interface_a._cable_peer = instance.interface_b # TODO: Rename _cable_peer field + instance.interface_a._cable_peer = instance.interface_b # TODO: Rename _cable_peer field instance.interface_a.save() if instance.interface_b.cable != instance: logger.debug(f"Updating interface B for wireless link {instance}") instance.interface_b.wireless_link = instance - # instance.interface_b._cable_peer = instance.interface_a + instance.interface_b._cable_peer = instance.interface_a instance.interface_b.save() + # Create/update cable paths + if created: + for interface in (instance.interface_a, instance.interface_b): + create_cablepath(interface) + @receiver(post_delete, sender=WirelessLink) def nullify_connected_interfaces(instance, **kwargs): """ When a WirelessLink is deleted, update its two connected Interfaces """ - print('nullify_connected_interfaces') logger = logging.getLogger('netbox.wireless.wirelesslink') if instance.interface_a is not None: @@ -56,3 +60,8 @@ def nullify_connected_interfaces(instance, **kwargs): _cable_peer_type=None, _cable_peer_id=None ) + + # Delete and retrace any dependent cable paths + for cablepath in CablePath.objects.filter(path__contains=instance): + print(f'Deleting cable path {cablepath.pk}') + cablepath.delete() From 1c73bd5079876cec632d8a7eafa71e01e701a369 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 13:39:14 -0400 Subject: [PATCH 08/28] Resolve test errors --- netbox/dcim/api/serializers.py | 4 ++-- netbox/dcim/graphql/types.py | 3 +++ netbox/wireless/api/serializers.py | 2 +- netbox/wireless/graphql/types.py | 9 +++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index edd73b87e4e..28d3a143b63 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -632,8 +632,8 @@ class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, Co parent = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) - rf_channel = ChoiceField(choices=WirelessChannelChoices) - rf_channel_width = ChoiceField(choices=WirelessChannelWidthChoices) + rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False) + rf_channel_width = ChoiceField(choices=WirelessChannelWidthChoices, required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index be10556bea0..55f1ba15049 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -206,6 +206,9 @@ class Meta: def resolve_mode(self, info): return self.mode or None + def resolve_rf_channel(self, info): + return self.rf_channel or None + class InterfaceTemplateType(ComponentTemplateObjectType): diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 90515f53e8e..9337d68646e 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -29,7 +29,7 @@ class WirelessLinkSerializer(PrimaryModelSerializer): interface_b = NestedInterfaceSerializer() class Meta: - model = WirelessLAN + model = WirelessLink fields = [ 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'description', ] diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 4cdb75ebe33..0afd8e69a12 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -3,6 +3,7 @@ __all__ = ( 'WirelessLANType', + 'WirelessLinkType', ) @@ -12,3 +13,11 @@ class Meta: model = models.WirelessLAN fields = '__all__' filterset_class = filtersets.WirelessLANFilterSet + + +class WirelessLinkType(ObjectType): + + class Meta: + model = models.WirelessLink + fields = '__all__' + filterset_class = filtersets.WirelessLinkFilterSet From ac2cd552b9641023e7ddf615fcb461e24fe42ea4 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 14:04:53 -0400 Subject: [PATCH 09/28] Rename cable_peer fields to link_peer --- netbox/circuits/api/serializers.py | 6 +- .../migrations/0003_rename_cable_peer.py | 23 +++++ netbox/circuits/models.py | 4 +- netbox/dcim/api/serializers.py | 54 +++++------ netbox/dcim/api/views.py | 12 +-- .../dcim/migrations/0139_rename_cable_peer.py | 93 +++++++++++++++++++ netbox/dcim/models/__init__.py | 2 +- netbox/dcim/models/cables.py | 2 +- netbox/dcim/models/device_components.py | 52 +++++------ netbox/dcim/models/power.py | 4 +- netbox/dcim/models/racks.py | 8 +- netbox/dcim/signals.py | 8 +- netbox/dcim/tables/devices.py | 51 +++++----- netbox/dcim/tables/power.py | 4 +- netbox/dcim/tables/template_code.py | 2 +- netbox/dcim/tests/test_models.py | 8 +- .../circuits/inc/circuit_termination.html | 2 +- netbox/wireless/models.py | 3 + netbox/wireless/signals.py | 12 +-- 19 files changed, 236 insertions(+), 114 deletions(-) create mode 100644 netbox/circuits/migrations/0003_rename_cable_peer.py create mode 100644 netbox/dcim/migrations/0139_rename_cable_peer.py diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index ac62856108c..e00b3dfc889 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -3,7 +3,7 @@ from circuits.choices import CircuitStatusChoices from circuits.models import * from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer -from dcim.api.serializers import CableTerminationSerializer +from dcim.api.serializers import LinkTerminationSerializer from netbox.api import ChoiceField from netbox.api.serializers import ( OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer @@ -90,7 +90,7 @@ class Meta: ] -class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSerializer): +class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') circuit = NestedCircuitSerializer() site = NestedSiteSerializer(required=False, allow_null=True) @@ -101,6 +101,6 @@ class Meta: model = CircuitTermination fields = [ 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', - 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', + 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', '_occupied', ] diff --git a/netbox/circuits/migrations/0003_rename_cable_peer.py b/netbox/circuits/migrations/0003_rename_cable_peer.py new file mode 100644 index 00000000000..475a84d0f81 --- /dev/null +++ b/netbox/circuits/migrations/0003_rename_cable_peer.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.8 on 2021-10-13 17:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0002_squashed_0029'), + ] + + operations = [ + migrations.RenameField( + model_name='circuittermination', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='circuittermination', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index bc7dcc2197c..8420db5636f 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -4,7 +4,7 @@ from django.urls import reverse from dcim.fields import ASNField -from dcim.models import CableTermination, PathEndpoint +from dcim.models import LinkTermination, PathEndpoint from extras.models import ObjectChange from extras.utils import extras_features from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel @@ -246,7 +246,7 @@ def get_status_class(self): @extras_features('webhooks') -class CircuitTermination(ChangeLoggedModel, CableTermination): +class CircuitTermination(ChangeLoggedModel, LinkTermination): circuit = models.ForeignKey( to='circuits.Circuit', on_delete=models.CASCADE, diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 28d3a143b63..9187901f038 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -22,25 +22,25 @@ from .nested_serializers import * -class CableTerminationSerializer(serializers.ModelSerializer): - cable_peer_type = serializers.SerializerMethodField(read_only=True) - cable_peer = serializers.SerializerMethodField(read_only=True) +class LinkTerminationSerializer(serializers.ModelSerializer): + link_peer_type = serializers.SerializerMethodField(read_only=True) + link_peer = serializers.SerializerMethodField(read_only=True) _occupied = serializers.SerializerMethodField(read_only=True) - def get_cable_peer_type(self, obj): - if obj._cable_peer is not None: - return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}' + def get_link_peer_type(self, obj): + if obj._link_peer is not None: + return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}' return None @swagger_serializer_method(serializer_or_field=serializers.DictField) - def get_cable_peer(self, obj): + def get_link_peer(self, obj): """ - Return the appropriate serializer for the cable termination model. + Return the appropriate serializer for the link termination model. """ - if obj._cable_peer is not None: - serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested') + if obj._link_peer is not None: + serializer = get_serializer_for_model(obj._link_peer, prefix='Nested') context = {'request': self.context['request']} - return serializer(obj._cable_peer, context=context).data + return serializer(obj._link_peer, context=context).data return None @swagger_serializer_method(serializer_or_field=serializers.BooleanField) @@ -529,7 +529,7 @@ class DeviceNAPALMSerializer(serializers.Serializer): # Device components # -class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): +class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -548,12 +548,12 @@ class Meta: model = ConsoleServerPort fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', - 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] -class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): +class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -572,12 +572,12 @@ class Meta: model = ConsolePort fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', - 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] -class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): +class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -601,12 +601,12 @@ class Meta: model = PowerOutlet fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', - 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] -class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): +class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') device = NestedDeviceSerializer() type = ChoiceField( @@ -620,12 +620,12 @@ class Meta: model = PowerPort fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', - 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', + 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] -class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): +class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=InterfaceTypeChoices) @@ -649,7 +649,7 @@ class Meta: fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'untagged_vlan', - 'tagged_vlans', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', + 'tagged_vlans', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied', ] @@ -668,7 +668,7 @@ def validate(self, data): return super().validate(data) -class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer): +class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=PortTypeChoices) @@ -678,7 +678,7 @@ class Meta: model = RearPort fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', + 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -694,7 +694,7 @@ class Meta: fields = ['id', 'url', 'display', 'name', 'label'] -class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer): +class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=PortTypeChoices) @@ -705,7 +705,7 @@ class Meta: model = FrontPort fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', - 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', + 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] @@ -881,7 +881,7 @@ class Meta: fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count'] -class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): +class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') power_panel = NestedPowerPanelSerializer() rack = NestedRackSerializer( @@ -911,7 +911,7 @@ class Meta: model = PowerFeed fields = [ 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', - 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', + 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 2b9d9734c5b..43ced046cf5 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -513,7 +513,7 @@ def napalm(self, request, pk): # class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') + queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filtersets.ConsolePortFilterSet brief_prefetch_fields = ['device'] @@ -521,7 +521,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): queryset = ConsoleServerPort.objects.prefetch_related( - 'device', '_path__destination', 'cable', '_cable_peer', 'tags' + 'device', '_path__destination', 'cable', '_link_peer', 'tags' ) serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filtersets.ConsoleServerPortFilterSet @@ -529,14 +529,14 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): class PowerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') + queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') serializer_class = serializers.PowerPortSerializer filterset_class = filtersets.PowerPortFilterSet brief_prefetch_fields = ['device'] class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') + queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filtersets.PowerOutletFilterSet brief_prefetch_fields = ['device'] @@ -544,7 +544,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, ModelViewSet): queryset = Interface.objects.prefetch_related( - 'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags' + 'device', 'parent', 'lag', '_path__destination', 'cable', '_link_peer', 'ip_addresses', 'tags' ) serializer_class = serializers.InterfaceSerializer filterset_class = filtersets.InterfaceFilterSet @@ -625,7 +625,7 @@ class PowerPanelViewSet(ModelViewSet): class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet): queryset = PowerFeed.objects.prefetch_related( - 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags' + 'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags' ) serializer_class = serializers.PowerFeedSerializer filterset_class = filtersets.PowerFeedFilterSet diff --git a/netbox/dcim/migrations/0139_rename_cable_peer.py b/netbox/dcim/migrations/0139_rename_cable_peer.py new file mode 100644 index 00000000000..62a4bacdd0a --- /dev/null +++ b/netbox/dcim/migrations/0139_rename_cable_peer.py @@ -0,0 +1,93 @@ +# Generated by Django 3.2.8 on 2021-10-13 17:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0138_interface_wireless_link'), + ] + + operations = [ + migrations.RenameField( + model_name='consoleport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='consoleport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='consoleserverport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='consoleserverport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='frontport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='frontport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='interface', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='interface', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='powerfeed', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='powerfeed', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='poweroutlet', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='poweroutlet', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='powerport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='powerport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + migrations.RenameField( + model_name='rearport', + old_name='_cable_peer_id', + new_name='_link_peer_id', + ), + migrations.RenameField( + model_name='rearport', + old_name='_cable_peer_type', + new_name='_link_peer_type', + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 0375a9fb466..58a3e1de5de 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -10,7 +10,7 @@ 'BaseInterface', 'Cable', 'CablePath', - 'CableTermination', + 'LinkTermination', 'ConsolePort', 'ConsolePortTemplate', 'ConsoleServerPort', diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 6c61c071292..fb3e71543e6 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -395,7 +395,7 @@ def from_origin(cls, origin): # Follow the link to its far-end termination path.append(object_to_path_node(node.link)) - peer_termination = node.get_cable_peer() + peer_termination = node.get_link_peer() # Follow a FrontPort to its corresponding RearPort if isinstance(peer_termination, FrontPort): diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 38e902f22b0..f8649b419ac 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -22,7 +22,7 @@ __all__ = ( 'BaseInterface', - 'CableTermination', + 'LinkTermination', 'ConsolePort', 'ConsoleServerPort', 'DeviceBay', @@ -87,14 +87,14 @@ def parent_object(self): return self.device -class CableTermination(models.Model): +class LinkTermination(models.Model): """ - An abstract model inherited by all models to which a Cable can terminate (certain device components, PowerFeed, and - CircuitTermination instances). The `cable` field indicates the Cable instance which is terminated to this instance. + An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples + include most device components, CircuitTerminations, and PowerFeeds. The `cable` and `wireless_link` fields + reference the attached Cable or WirelessLink instance, respectively. - `_cable_peer` is a GenericForeignKey used to cache the far-end CableTermination on the local instance; this is a - shortcut to referencing `cable.termination_b`, for example. `_cable_peer` is set or cleared by the receivers in - dcim.signals when a Cable instance is created or deleted, respectively. + `_link_peer` is a GenericForeignKey used to cache the far-end LinkTermination on the local instance; this is a + shortcut to referencing `instance.link.termination_b`, for example. """ cable = models.ForeignKey( to='dcim.Cable', @@ -103,20 +103,20 @@ class CableTermination(models.Model): blank=True, null=True ) - _cable_peer_type = models.ForeignKey( + _link_peer_type = models.ForeignKey( to=ContentType, on_delete=models.SET_NULL, related_name='+', blank=True, null=True ) - _cable_peer_id = models.PositiveIntegerField( + _link_peer_id = models.PositiveIntegerField( blank=True, null=True ) - _cable_peer = GenericForeignKey( - ct_field='_cable_peer_type', - fk_field='_cable_peer_id' + _link_peer = GenericForeignKey( + ct_field='_link_peer_type', + fk_field='_link_peer_id' ) mark_connected = models.BooleanField( default=False, @@ -146,8 +146,8 @@ def clean(self): "mark_connected": "Cannot mark as connected with a cable attached." }) - def get_cable_peer(self): - return self._cable_peer + def get_link_peer(self): + return self._link_peer @property def _occupied(self): @@ -226,7 +226,7 @@ def connected_endpoint(self): # @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class ConsolePort(ComponentModel, CableTermination, PathEndpoint): +class ConsolePort(ComponentModel, LinkTermination, PathEndpoint): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. """ @@ -258,7 +258,7 @@ def get_absolute_url(self): # @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint): +class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. """ @@ -290,7 +290,7 @@ def get_absolute_url(self): # @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class PowerPort(ComponentModel, CableTermination, PathEndpoint): +class PowerPort(ComponentModel, LinkTermination, PathEndpoint): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. """ @@ -340,8 +340,8 @@ def get_power_draw(self): poweroutlet_ct = ContentType.objects.get_for_model(PowerOutlet) outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) utilization = PowerPort.objects.filter( - _cable_peer_type=poweroutlet_ct, - _cable_peer_id__in=outlet_ids + _link_peer_type=poweroutlet_ct, + _link_peer_id__in=outlet_ids ).aggregate( maximum_draw_total=Sum('maximum_draw'), allocated_draw_total=Sum('allocated_draw'), @@ -354,12 +354,12 @@ def get_power_draw(self): } # Calculate per-leg aggregates for three-phase feeds - if getattr(self._cable_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE: + if getattr(self._link_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE: for leg, leg_name in PowerOutletFeedLegChoices: outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) utilization = PowerPort.objects.filter( - _cable_peer_type=poweroutlet_ct, - _cable_peer_id__in=outlet_ids + _link_peer_type=poweroutlet_ct, + _link_peer_id__in=outlet_ids ).aggregate( maximum_draw_total=Sum('maximum_draw'), allocated_draw_total=Sum('allocated_draw'), @@ -387,7 +387,7 @@ def get_power_draw(self): # @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class PowerOutlet(ComponentModel, CableTermination, PathEndpoint): +class PowerOutlet(ComponentModel, LinkTermination, PathEndpoint): """ A physical power outlet (output) within a Device which provides power to a PowerPort. """ @@ -482,7 +482,7 @@ def count_ipaddresses(self): @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): +class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint): """ A network interface within a Device. A physical Interface can connect to exactly one other Interface. """ @@ -674,7 +674,7 @@ def link(self): # @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class FrontPort(ComponentModel, CableTermination): +class FrontPort(ComponentModel, LinkTermination): """ A pass-through port on the front of a Device. """ @@ -728,7 +728,7 @@ def clean(self): @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class RearPort(ComponentModel, CableTermination): +class RearPort(ComponentModel, LinkTermination): """ A pass-through port on the rear of a Device. """ diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 0e9520b36dc..6d6a04cea9f 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -10,7 +10,7 @@ from netbox.models import PrimaryModel from utilities.querysets import RestrictedQuerySet from utilities.validators import ExclusionValidator -from .device_components import CableTermination, PathEndpoint +from .device_components import LinkTermination, PathEndpoint __all__ = ( 'PowerFeed', @@ -67,7 +67,7 @@ def clean(self): @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class PowerFeed(PrimaryModel, PathEndpoint, CableTermination): +class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination): """ An electrical circuit delivered from a PowerPanel. """ diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index c287d7d6c73..94e7bf53a1f 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -422,13 +422,13 @@ def get_power_utilization(self): return 0 pf_powerports = PowerPort.objects.filter( - _cable_peer_type=ContentType.objects.get_for_model(PowerFeed), - _cable_peer_id__in=powerfeeds.values_list('id', flat=True) + _link_peer_type=ContentType.objects.get_for_model(PowerFeed), + _link_peer_id__in=powerfeeds.values_list('id', flat=True) ) poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports) allocated_draw_total = PowerPort.objects.filter( - _cable_peer_type=ContentType.objects.get_for_model(PowerOutlet), - _cable_peer_id__in=poweroutlets.values_list('id', flat=True) + _link_peer_type=ContentType.objects.get_for_model(PowerOutlet), + _link_peer_id__in=poweroutlets.values_list('id', flat=True) ).aggregate(Sum('allocated_draw'))['allocated_draw__sum'] or 0 return int(allocated_draw_total / available_power_total * 100) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 942bf04e4f1..6165465251b 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -83,12 +83,12 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs): if instance.termination_a.cable != instance: logger.debug(f"Updating termination A for cable {instance}") instance.termination_a.cable = instance - instance.termination_a._cable_peer = instance.termination_b + instance.termination_a._link_peer = instance.termination_b instance.termination_a.save() if instance.termination_b.cable != instance: logger.debug(f"Updating termination B for cable {instance}") instance.termination_b.cable = instance - instance.termination_b._cable_peer = instance.termination_a + instance.termination_b._link_peer = instance.termination_a instance.termination_b.save() # Create/update cable paths @@ -119,11 +119,11 @@ def nullify_connected_endpoints(instance, **kwargs): if instance.termination_a is not None: logger.debug(f"Nullifying termination A for cable {instance}") model = instance.termination_a._meta.model - model.objects.filter(pk=instance.termination_a.pk).update(_cable_peer_type=None, _cable_peer_id=None) + model.objects.filter(pk=instance.termination_a.pk).update(_link_peer_type=None, _link_peer_id=None) if instance.termination_b is not None: logger.debug(f"Nullifying termination B for cable {instance}") model = instance.termination_b._meta.model - model.objects.filter(pk=instance.termination_b.pk).update(_cable_peer_type=None, _cable_peer_id=None) + model.objects.filter(pk=instance.termination_b.pk).update(_link_peer_type=None, _link_peer_id=None) # Delete and retrace any dependent cable paths for cablepath in CablePath.objects.filter(path__contains=instance): diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 1eae62a05e8..a375a77cc9d 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -12,7 +12,7 @@ MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn, ) from .template_code import ( - CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS, + LINKTERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS, FRONTPORT_BUTTONS, INTERFACE_BUTTONS, INTERFACE_IPADDRESSES, INTERFACE_TAGGED_VLANS, POWEROUTLET_BUTTONS, POWERPORT_BUTTONS, REARPORT_BUTTONS, ) @@ -258,11 +258,11 @@ class CableTerminationTable(BaseTable): orderable=False, verbose_name='Cable Color' ) - cable_peer = TemplateColumn( - accessor='_cable_peer', - template_code=CABLETERMINATION, + link_peer = TemplateColumn( + accessor='_link_peer', + template_code=LINKTERMINATION, orderable=False, - verbose_name='Cable Peer' + verbose_name='Link Peer' ) mark_connected = BooleanColumn() @@ -270,7 +270,7 @@ class CableTerminationTable(BaseTable): class PathEndpointTable(CableTerminationTable): connection = TemplateColumn( accessor='_path.last_node', - template_code=CABLETERMINATION, + template_code=LINKTERMINATION, verbose_name='Connection', orderable=False ) @@ -291,7 +291,7 @@ class Meta(DeviceComponentTable.Meta): model = ConsolePort fields = ( 'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', - 'cable_peer', 'connection', 'tags', + 'link_peer', 'connection', 'tags', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') @@ -312,7 +312,7 @@ class Meta(DeviceComponentTable.Meta): model = ConsolePort fields = ( 'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', - 'cable_peer', 'connection', 'tags', 'actions' + 'link_peer', 'connection', 'tags', 'actions' ) default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') row_attrs = { @@ -335,7 +335,7 @@ class Meta(DeviceComponentTable.Meta): model = ConsoleServerPort fields = ( 'pk', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', - 'cable_peer', 'connection', 'tags', + 'link_peer', 'connection', 'tags', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') @@ -357,7 +357,7 @@ class Meta(DeviceComponentTable.Meta): model = ConsoleServerPort fields = ( 'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', - 'cable_peer', 'connection', 'tags', 'actions', + 'link_peer', 'connection', 'tags', 'actions', ) default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') row_attrs = { @@ -380,7 +380,7 @@ class Meta(DeviceComponentTable.Meta): model = PowerPort fields = ( 'pk', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw', - 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', + 'cable', 'cable_color', 'link_peer', 'connection', 'tags', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') @@ -402,7 +402,7 @@ class Meta(DeviceComponentTable.Meta): model = PowerPort fields = ( 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable', - 'cable_color', 'cable_peer', 'connection', 'tags', 'actions', + 'cable_color', 'link_peer', 'connection', 'tags', 'actions', ) default_columns = ( 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection', @@ -431,7 +431,7 @@ class Meta(DeviceComponentTable.Meta): model = PowerOutlet fields = ( 'pk', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable', - 'cable_color', 'cable_peer', 'connection', 'tags', + 'cable_color', 'link_peer', 'connection', 'tags', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description') @@ -452,7 +452,7 @@ class Meta(DeviceComponentTable.Meta): model = PowerOutlet fields = ( 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', - 'cable_color', 'cable_peer', 'connection', 'tags', 'actions', + 'cable_color', 'link_peer', 'connection', 'tags', 'actions', ) default_columns = ( 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions', @@ -485,6 +485,9 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable } ) mgmt_only = BooleanColumn() + wireless_link = tables.Column( + linkify=True + ) tags = TagColumn( url_name='dcim:interface_list' ) @@ -493,8 +496,8 @@ class Meta(DeviceComponentTable.Meta): model = Interface fields = ( 'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', - 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', + 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', + 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') @@ -525,8 +528,8 @@ class Meta(DeviceComponentTable.Meta): model = Interface fields = ( 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', - 'untagged_vlan', 'tagged_vlans', 'actions', + 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'link_peer', 'connection', 'tags', + 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', ) order_by = ('name',) default_columns = ( @@ -562,7 +565,7 @@ class Meta(DeviceComponentTable.Meta): model = FrontPort fields = ( 'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', - 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags', + 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', ) default_columns = ( 'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', @@ -586,10 +589,10 @@ class Meta(DeviceComponentTable.Meta): model = FrontPort fields = ( 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable', - 'cable_color', 'cable_peer', 'tags', 'actions', + 'cable_color', 'link_peer', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer', + 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer', 'actions', ) row_attrs = { @@ -613,7 +616,7 @@ class Meta(DeviceComponentTable.Meta): model = RearPort fields = ( 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable', - 'cable_color', 'cable_peer', 'tags', + 'cable_color', 'link_peer', 'tags', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description') @@ -635,10 +638,10 @@ class Meta(DeviceComponentTable.Meta): model = RearPort fields = ( 'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color', - 'cable_peer', 'tags', 'actions', + 'link_peer', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions', + 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions', ) row_attrs = { 'class': get_cabletermination_row_class diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py index b8e032e7ff0..95628291128 100644 --- a/netbox/dcim/tables/power.py +++ b/netbox/dcim/tables/power.py @@ -71,10 +71,10 @@ class Meta(BaseTable.Meta): model = PowerFeed fields = ( 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', - 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power', + 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power', 'comments', 'tags', ) default_columns = ( 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', - 'cable_peer', + 'link_peer', ) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 9e2dc519bfd..7e78cb97dbe 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -1,4 +1,4 @@ -CABLETERMINATION = """ +LINKTERMINATION = """ {% if value %} {% if value.parent_object %} {{ value.parent_object }} diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index ae280365eac..1042057de53 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -494,9 +494,9 @@ def test_cable_creation(self): interface1 = Interface.objects.get(pk=self.interface1.pk) interface2 = Interface.objects.get(pk=self.interface2.pk) self.assertEqual(self.cable.termination_a, interface1) - self.assertEqual(interface1._cable_peer, interface2) + self.assertEqual(interface1._link_peer, interface2) self.assertEqual(self.cable.termination_b, interface2) - self.assertEqual(interface2._cable_peer, interface1) + self.assertEqual(interface2._link_peer, interface1) def test_cable_deletion(self): """ @@ -508,10 +508,10 @@ def test_cable_deletion(self): self.assertNotEqual(str(self.cable), '#None') interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertIsNone(interface1.cable) - self.assertIsNone(interface1._cable_peer) + self.assertIsNone(interface1._link_peer) interface2 = Interface.objects.get(pk=self.interface2.pk) self.assertIsNone(interface2.cable) - self.assertIsNone(interface2._cable_peer) + self.assertIsNone(interface2._link_peer) def test_cabletermination_deletion(self): """ diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index e2fe6af29a0..5c224f7c01f 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -45,7 +45,7 @@ Marked as connected {% elif termination.cable %} {{ termination.cable }} - {% with peer=termination.get_cable_peer %} + {% with peer=termination.get_link_peer %} to {% if peer.device %} {{ peer.device }}
diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 41f3dbf6da8..f8c947385ce 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -98,6 +98,9 @@ class Meta: ordering = ['pk'] unique_together = ('interface_a', 'interface_b') + def __str__(self): + return f'#{self.pk}' + def get_absolute_url(self): return reverse('wireless:wirelesslink', args=[self.pk]) diff --git a/netbox/wireless/signals.py b/netbox/wireless/signals.py index b8dc8a18660..935e116771b 100644 --- a/netbox/wireless/signals.py +++ b/netbox/wireless/signals.py @@ -25,12 +25,12 @@ def update_connected_interfaces(instance, created, raw=False, **kwargs): if instance.interface_a.wireless_link != instance: logger.debug(f"Updating interface A for wireless link {instance}") instance.interface_a.wireless_link = instance - instance.interface_a._cable_peer = instance.interface_b # TODO: Rename _cable_peer field + instance.interface_a._link_peer = instance.interface_b instance.interface_a.save() if instance.interface_b.cable != instance: logger.debug(f"Updating interface B for wireless link {instance}") instance.interface_b.wireless_link = instance - instance.interface_b._cable_peer = instance.interface_a + instance.interface_b._link_peer = instance.interface_a instance.interface_b.save() # Create/update cable paths @@ -50,15 +50,15 @@ def nullify_connected_interfaces(instance, **kwargs): logger.debug(f"Nullifying interface A for wireless link {instance}") Interface.objects.filter(pk=instance.interface_a.pk).update( wireless_link=None, - _cable_peer_type=None, - _cable_peer_id=None + _link_peer_type=None, + _link_peer_id=None ) if instance.interface_b is not None: logger.debug(f"Nullifying interface B for wireless link {instance}") Interface.objects.filter(pk=instance.interface_b.pk).update( wireless_link=None, - _cable_peer_type=None, - _cable_peer_id=None + _link_peer_type=None, + _link_peer_id=None ) # Delete and retrace any dependent cable paths From ec0560a2c547c6f480ab67b025fa979a939e5f3c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 14:16:10 -0400 Subject: [PATCH 10/28] Fix trace_paths command for wireless links --- netbox/dcim/management/commands/trace_paths.py | 6 +++++- netbox/dcim/tables/template_code.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/management/commands/trace_paths.py b/netbox/dcim/management/commands/trace_paths.py index fd5f9cfab4f..d0cd644868f 100644 --- a/netbox/dcim/management/commands/trace_paths.py +++ b/netbox/dcim/management/commands/trace_paths.py @@ -1,6 +1,7 @@ from django.core.management.base import BaseCommand from django.core.management.color import no_style from django.db import connection +from django.db.models import Q from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort from dcim.signals import create_cablepath @@ -67,7 +68,10 @@ def handle(self, *model_names, **options): # Retrace paths for model in ENDPOINT_MODELS: - origins = model.objects.filter(cable__isnull=False) + params = Q(cable__isnull=False) + if hasattr(model, 'wireless_link'): + params |= Q(wireless_link__isnull=False) + origins = model.objects.filter(params) if not options['force']: origins = origins.filter(_path__isnull=True) origins_count = origins.count() diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 7e78cb97dbe..a5a4d9979ef 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -195,8 +195,10 @@ {% endif %} -{% if record.cable %} +{% if record.link %} +{% endif %} +{% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} {% if perms.dcim.delete_cable %} From 95ed07a95ec0fe2f71441311cb3e6bb9047ef17a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 14:31:30 -0400 Subject: [PATCH 11/28] Add status field to WirelessLink --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/choices.py | 4 ++-- netbox/dcim/filtersets.py | 2 +- netbox/dcim/forms/bulk_edit.py | 2 +- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/filtersets.py | 2 +- netbox/dcim/models/cables.py | 8 ++++---- netbox/dcim/signals.py | 4 ++-- netbox/dcim/tests/test_cablepaths.py | 6 +++--- netbox/dcim/tests/test_filtersets.py | 16 ++++++++-------- netbox/dcim/tests/test_views.py | 4 ++-- netbox/templates/wireless/wirelesslink.html | 4 ++++ netbox/wireless/api/serializers.py | 5 ++++- netbox/wireless/filtersets.py | 4 ++++ netbox/wireless/forms/bulk_edit.py | 11 ++++++++--- netbox/wireless/forms/bulk_import.py | 7 ++++++- netbox/wireless/forms/filtersets.py | 8 +++++++- netbox/wireless/forms/models.py | 7 +++++-- netbox/wireless/migrations/0001_wireless.py | 1 + netbox/wireless/models.py | 11 +++++++++++ netbox/wireless/tables.py | 7 ++++--- 21 files changed, 80 insertions(+), 37 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9187901f038..03d14a160d4 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -757,7 +757,7 @@ class CableSerializer(PrimaryModelSerializer): ) termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) - status = ChoiceField(choices=CableStatusChoices, required=False) + status = ChoiceField(choices=LinkStatusChoices, required=False) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) class Meta: diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 9a78a74f9d4..d58d5466d01 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1030,7 +1030,7 @@ class PortTypeChoices(ChoiceSet): # -# Cables +# Cables/links # class CableTypeChoices(ChoiceSet): @@ -1094,7 +1094,7 @@ class CableTypeChoices(ChoiceSet): ) -class CableStatusChoices(ChoiceSet): +class LinkStatusChoices(ChoiceSet): STATUS_CONNECTED = 'connected' STATUS_PLANNED = 'planned' diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 0c756957a89..f778fd083c9 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1205,7 +1205,7 @@ class CableFilterSet(PrimaryModelFilterSet): choices=CableTypeChoices ) status = django_filters.MultipleChoiceFilter( - choices=CableStatusChoices + choices=LinkStatusChoices ) color = django_filters.MultipleChoiceFilter( choices=ColorChoices diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 67a482a266d..f710e7d8cff 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -453,7 +453,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE widget=StaticSelect() ) status = forms.ChoiceField( - choices=add_blank_choice(CableStatusChoices), + choices=add_blank_choice(LinkStatusChoices), required=False, widget=StaticSelect(), initial='' diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index a2685c8e0a0..95b4dd136cb 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -807,7 +807,7 @@ class CableCSVForm(CustomFieldModelCSVForm): # Cable attributes status = CSVChoiceField( - choices=CableStatusChoices, + choices=LinkStatusChoices, required=False, help_text='Connection status' ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 605139c1bea..169b93028ec 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -732,7 +732,7 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): ) status = forms.ChoiceField( required=False, - choices=add_blank_choice(CableStatusChoices), + choices=add_blank_choice(LinkStatusChoices), widget=StaticSelect() ) color = ColorField( diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index fb3e71543e6..129617746e7 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -64,8 +64,8 @@ class Cable(PrimaryModel): ) status = models.CharField( max_length=50, - choices=CableStatusChoices, - default=CableStatusChoices.STATUS_CONNECTED + choices=LinkStatusChoices, + default=LinkStatusChoices.STATUS_CONNECTED ) label = models.CharField( max_length=100, @@ -285,7 +285,7 @@ def save(self, *args, **kwargs): self._pk = self.pk def get_status_class(self): - return CableStatusChoices.CSS_CLASSES.get(self.status) + return LinkStatusChoices.CSS_CLASSES.get(self.status) def get_compatible_types(self): """ @@ -390,7 +390,7 @@ def from_origin(cls, origin): node = origin while node.link is not None: - if hasattr(node.link, 'status') and node.link.status != CableStatusChoices.STATUS_CONNECTED: + if hasattr(node.link, 'status') and node.link.status != LinkStatusChoices.STATUS_CONNECTED: is_active = False # Follow the link to its far-end termination diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 6165465251b..79e9c6687ef 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -4,7 +4,7 @@ from django.db.models.signals import post_save, post_delete, pre_delete from django.dispatch import receiver -from .choices import CableStatusChoices +from .choices import LinkStatusChoices from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis from .utils import create_cablepath, rebuild_paths @@ -102,7 +102,7 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs): # We currently don't support modifying either termination of an existing Cable. (This # may change in the future.) However, we do need to capture status changes and update # any CablePaths accordingly. - if instance.status != CableStatusChoices.STATUS_CONNECTED: + if instance.status != LinkStatusChoices.STATUS_CONNECTED: CablePath.objects.filter(path__contains=instance).update(is_active=False) else: rebuild_paths(instance) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index c0fc89f8379..6849df012fe 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2,7 +2,7 @@ from django.test import TestCase from circuits.models import * -from dcim.choices import CableStatusChoices +from dcim.choices import LinkStatusChoices from dcim.models import * from dcim.utils import object_to_path_node @@ -1142,7 +1142,7 @@ def test_302_update_path_on_cable_status_change(self): self.assertEqual(CablePath.objects.count(), 2) # Change cable 2's status to "planned" - cable2.status = CableStatusChoices.STATUS_PLANNED + cable2.status = LinkStatusChoices.STATUS_PLANNED cable2.save() self.assertPathExists( origin=interface1, @@ -1160,7 +1160,7 @@ def test_302_update_path_on_cable_status_change(self): # Change cable 2's status to "connected" cable2 = Cable.objects.get(pk=cable2.pk) - cable2.status = CableStatusChoices.STATUS_CONNECTED + cable2.status = LinkStatusChoices.STATUS_CONNECTED cable2.save() self.assertPathExists( origin=interface1, diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index fb94bde0842..0c1ffd54b21 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2855,12 +2855,12 @@ def setUpTestData(cls): console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1') # Cables - Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() - Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save() def test_label(self): @@ -2880,9 +2880,9 @@ def test_type(self): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_status(self): - params = {'status': [CableStatusChoices.STATUS_CONNECTED]} + params = {'status': [LinkStatusChoices.STATUS_CONNECTED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - params = {'status': [CableStatusChoices.STATUS_PLANNED]} + params = {'status': [LinkStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_color(self): diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 545a56f81a9..c0af2d4383b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1813,7 +1813,7 @@ def setUpTestData(cls): 'termination_b_type': interface_ct.pk, 'termination_b_id': interfaces[3].pk, 'type': CableTypeChoices.TYPE_CAT6, - 'status': CableStatusChoices.STATUS_PLANNED, + 'status': LinkStatusChoices.STATUS_PLANNED, 'label': 'Label', 'color': 'c0c0c0', 'length': 100, @@ -1830,7 +1830,7 @@ def setUpTestData(cls): cls.bulk_edit_data = { 'type': CableTypeChoices.TYPE_CAT5E, - 'status': CableStatusChoices.STATUS_CONNECTED, + 'status': LinkStatusChoices.STATUS_CONNECTED, 'label': 'New label', 'color': '00ff00', 'length': 50, diff --git a/netbox/templates/wireless/wirelesslink.html b/netbox/templates/wireless/wirelesslink.html index 6196adae4e4..45ec6b0c906 100644 --- a/netbox/templates/wireless/wirelesslink.html +++ b/netbox/templates/wireless/wirelesslink.html @@ -15,6 +15,10 @@
Interface A
Link Properties
+ + + + diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 9337d68646e..5a733012926 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,7 +1,9 @@ from rest_framework import serializers +from dcim.choices import LinkStatusChoices from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer +from netbox.api import ChoiceField from netbox.api.serializers import PrimaryModelSerializer from wireless.models import * from .nested_serializers import * @@ -25,11 +27,12 @@ class Meta: class WirelessLinkSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') + status = ChoiceField(choices=LinkStatusChoices, required=False) interface_a = NestedInterfaceSerializer() interface_b = NestedInterfaceSerializer() class Meta: model = WirelessLink fields = [ - 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'description', + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', ] diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 7341ada9df7..f765172dd68 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -1,6 +1,7 @@ import django_filters from django.db.models import Q +from dcim.choices import LinkStatusChoices from extras.filters import TagFilter from netbox.filtersets import PrimaryModelFilterSet from .models import * @@ -37,6 +38,9 @@ class WirelessLinkFilterSet(PrimaryModelFilterSet): method='search', label='Search', ) + status = django_filters.MultipleChoiceFilter( + choices=LinkStatusChoices + ) tag = TagFilter() class Meta: diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 65666ccb1c6..72af81f566f 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -1,10 +1,11 @@ from django import forms -from dcim.models import * +from dcim.choices import LinkStatusChoices from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import VLAN from utilities.forms import BootstrapMixin, DynamicModelChoiceField from wireless.constants import SSID_MAX_LENGTH +from wireless.models import * __all__ = ( 'WirelessLANBulkEditForm', @@ -14,7 +15,7 @@ class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( - queryset=PowerFeed.objects.all(), + queryset=WirelessLAN.objects.all(), widget=forms.MultipleHiddenInput ) vlan = DynamicModelChoiceField( @@ -35,13 +36,17 @@ class Meta: class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( - queryset=PowerFeed.objects.all(), + queryset=WirelessLink.objects.all(), widget=forms.MultipleHiddenInput ) ssid = forms.CharField( max_length=SSID_MAX_LENGTH, required=False ) + status = forms.ChoiceField( + choices=LinkStatusChoices, + required=False + ) description = forms.CharField( required=False ) diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index caf322dc1b7..763305c38d8 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -1,7 +1,8 @@ +from dcim.choices import LinkStatusChoices from dcim.models import Interface from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN -from utilities.forms import CSVModelChoiceField +from utilities.forms import CSVChoiceField, CSVModelChoiceField from wireless.models import * __all__ = ( @@ -23,6 +24,10 @@ class Meta: class WirelessLinkCSVForm(CustomFieldModelCSVForm): + status = CSVChoiceField( + choices=LinkStatusChoices, + help_text='Connection status' + ) interface_a = CSVModelChoiceField( queryset=Interface.objects.all() ) diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index fa191209991..6da3cd716fd 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -1,8 +1,9 @@ from django import forms from django.utils.translation import gettext as _ +from dcim.choices import LinkStatusChoices from extras.forms import CustomFieldModelFilterForm -from utilities.forms import BootstrapMixin, TagFilterField +from utilities.forms import add_blank_choice, BootstrapMixin, StaticSelect, TagFilterField from wireless.models import * __all__ = ( @@ -42,4 +43,9 @@ class WirelessLinkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): required=False, label='SSID' ) + status = forms.ChoiceField( + required=False, + choices=add_blank_choice(LinkStatusChoices), + widget=StaticSelect() + ) tag = TagFilterField(model) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index c494fb5a266..174eb5983f8 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -2,7 +2,7 @@ from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN -from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect from wireless.models import * __all__ = ( @@ -55,5 +55,8 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLink fields = [ - 'interface_a', 'interface_b', 'ssid', 'description', 'tags', + 'interface_a', 'interface_b', 'status', 'ssid', 'description', 'tags', ] + widgets = { + 'status': StaticSelect, + } diff --git a/netbox/wireless/migrations/0001_wireless.py b/netbox/wireless/migrations/0001_wireless.py index 2fb07e5fde2..8eb042d7d31 100644 --- a/netbox/wireless/migrations/0001_wireless.py +++ b/netbox/wireless/migrations/0001_wireless.py @@ -42,6 +42,7 @@ class Migration(migrations.Migration): ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), ('id', models.BigAutoField(primary_key=True, serialize=False)), ('ssid', models.CharField(blank=True, max_length=32)), + ('status', models.CharField(default='connected', max_length=50)), ('description', models.CharField(blank=True, max_length=200)), ('_interface_a_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')), ('_interface_b_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')), diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index f8c947385ce..d02358f1cd8 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -2,6 +2,7 @@ from django.db import models from django.urls import reverse +from dcim.choices import LinkStatusChoices from dcim.constants import WIRELESS_IFACE_TYPES from extras.utils import extras_features from netbox.models import BigIDModel, PrimaryModel @@ -70,6 +71,11 @@ class WirelessLink(PrimaryModel): blank=True, verbose_name='SSID' ) + status = models.CharField( + max_length=50, + choices=LinkStatusChoices, + default=LinkStatusChoices.STATUS_CONNECTED + ) description = models.CharField( max_length=200, blank=True @@ -94,6 +100,8 @@ class WirelessLink(PrimaryModel): objects = RestrictedQuerySet.as_manager() + clone_fields = ('ssid', 'status') + class Meta: ordering = ['pk'] unique_together = ('interface_a', 'interface_b') @@ -104,6 +112,9 @@ def __str__(self): def get_absolute_url(self): return reverse('wireless:wirelesslink', args=[self.pk]) + def get_status_class(self): + return LinkStatusChoices.CSS_CLASSES.get(self.status) + def clean(self): # Validate interface types diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 31c9e56a84d..9b0ef729117 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from .models import * -from utilities.tables import BaseTable, TagColumn, ToggleColumn +from utilities.tables import BaseTable, ChoiceFieldColumn, TagColumn, ToggleColumn __all__ = ( 'WirelessLANTable', @@ -30,6 +30,7 @@ class WirelessLinkTable(BaseTable): linkify=True, verbose_name='ID' ) + status = ChoiceFieldColumn() interface_a = tables.Column( linkify=True ) @@ -42,5 +43,5 @@ class WirelessLinkTable(BaseTable): class Meta(BaseTable.Meta): model = WirelessLink - fields = ('pk', 'id', 'interface_a', 'interface_b', 'ssid', 'description') - default_columns = ('pk', 'id', 'interface_a', 'interface_b', 'ssid', 'description') + fields = ('pk', 'id', 'status', 'interface_a', 'interface_b', 'ssid', 'description') + default_columns = ('pk', 'id', 'status', 'interface_a', 'interface_b', 'ssid', 'description') From 43f2d4a331253ababf5a2f7e3dcb809cfa98200d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 15:00:35 -0400 Subject: [PATCH 12/28] Add SVG trace support for WirelessLinks --- netbox/dcim/svg.py | 81 +++++++++++++++---- netbox/project-static/dist/cable_trace.css | 2 +- netbox/project-static/styles/cable-trace.scss | 7 +- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/svg.py b/netbox/dcim/svg.py index 5601bc5917e..b7f1576eed9 100644 --- a/netbox/dcim/svg.py +++ b/netbox/dcim/svg.py @@ -398,6 +398,39 @@ def _draw_cable(self, color, url, labels): return group + def _draw_wirelesslink(self, url, labels): + """ + Draw a line with labels representing a WirelessLink. + + :param url: Hyperlink URL + :param labels: Iterable of text labels + """ + group = Group(class_='connector') + + # Draw the wireless link + start = (OFFSET + self.center, self.cursor) + height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2 + end = (start[0], start[1] + height) + line = Line(start=start, end=end, class_='wireless-link') + group.add(line) + + self.cursor += PADDING * 2 + + # Add link + link = Hyperlink(href=f'{self.base_url}{url}', target='_blank') + + # Add text label(s) + for i, label in enumerate(labels): + self.cursor += LINE_HEIGHT + text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2) + text = Text(label, insert=text_coords, class_='bold' if not i else []) + link.add(text) + + group.add(link) + self.cursor += PADDING * 2 + + return group + def _draw_attachment(self): """ Return an SVG group containing a line element and "Attachment" label. @@ -418,6 +451,9 @@ def render(self): """ Return an SVG document representing a cable trace. """ + from dcim.models import Cable + from wireless.models import WirelessLink + traced_path = self.origin.trace() # Prep elements list @@ -452,24 +488,39 @@ def render(self): ) terminations.append(termination) - # Connector (either a Cable or attachment to a ProviderNetwork) + # Connector (a Cable or WirelessLink) if connector is not None: # Cable - cable_labels = [ - f'Cable {connector}', - connector.get_status_display() - ] - if connector.type: - cable_labels.append(connector.get_type_display()) - if connector.length and connector.length_unit: - cable_labels.append(f'{connector.length} {connector.get_length_unit_display()}') - cable = self._draw_cable( - color=connector.color or '000000', - url=connector.get_absolute_url(), - labels=cable_labels - ) - connectors.append(cable) + if type(connector) is Cable: + connector_labels = [ + f'Cable {connector}', + connector.get_status_display() + ] + if connector.type: + connector_labels.append(connector.get_type_display()) + if connector.length and connector.length_unit: + connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}') + cable = self._draw_cable( + color=connector.color or '000000', + url=connector.get_absolute_url(), + labels=connector_labels + ) + connectors.append(cable) + + # WirelessLink + elif type(connector) is WirelessLink: + connector_labels = [ + f'Wireless link {connector}', + connector.get_status_display() + ] + if connector.ssid: + connector_labels.append(connector.ssid) + wirelesslink = self._draw_wirelesslink( + url=connector.get_absolute_url(), + labels=connector_labels + ) + connectors.append(wirelesslink) # Far end termination termination = self._draw_box( diff --git a/netbox/project-static/dist/cable_trace.css b/netbox/project-static/dist/cable_trace.css index 633ccd57232..50622f1284d 100644 --- a/netbox/project-static/dist/cable_trace.css +++ b/netbox/project-static/dist/cable_trace.css @@ -1 +1 @@ -:root{--nbx-trace-color: #000;--nbx-trace-node-bg: #e9ecef;--nbx-trace-termination-bg: #f8f9fa;--nbx-trace-cable-shadow: #343a40;--nbx-trace-attachment: #ced4da}:root[data-netbox-color-mode=dark]{--nbx-trace-color: #fff;--nbx-trace-node-bg: #212529;--nbx-trace-termination-bg: #343a40;--nbx-trace-cable-shadow: #e9ecef;--nbx-trace-attachment: #6c757d}*{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:.875rem}text{text-anchor:middle;dominant-baseline:middle}text:not([fill]){fill:var(--nbx-trace-color)}text.bold{font-weight:700}svg rect{fill:var(--nbx-trace-node-bg);stroke:#606060;stroke-width:1}svg rect .termination{fill:var(--nbx-trace-termination-bg)}svg .connector text{text-anchor:start}svg line{stroke-width:5px}svg line.cable-shadow{stroke:var(--nbx-trace-cable-shadow);stroke-width:7px}svg line.attachment{stroke:var(--nbx-trace-attachment);stroke-dasharray:5px,5px} +:root{--nbx-trace-color: #000;--nbx-trace-node-bg: #e9ecef;--nbx-trace-termination-bg: #f8f9fa;--nbx-trace-cable-shadow: #343a40;--nbx-trace-attachment: #ced4da}:root[data-netbox-color-mode=dark]{--nbx-trace-color: #fff;--nbx-trace-node-bg: #212529;--nbx-trace-termination-bg: #343a40;--nbx-trace-cable-shadow: #e9ecef;--nbx-trace-attachment: #6c757d}*{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:.875rem}text{text-anchor:middle;dominant-baseline:middle}text:not([fill]){fill:var(--nbx-trace-color)}text.bold{font-weight:700}svg rect{fill:var(--nbx-trace-node-bg);stroke:#606060;stroke-width:1}svg rect .termination{fill:var(--nbx-trace-termination-bg)}svg .connector text{text-anchor:start}svg line{stroke-width:5px}svg line.cable-shadow{stroke:var(--nbx-trace-cable-shadow);stroke-width:7px}svg line.wireless-link{stroke:var(--nbx-trace-attachment);stroke-dasharray:4px 12px;stroke-linecap:round}svg line.attachment{stroke:var(--nbx-trace-attachment);stroke-dasharray:5px} diff --git a/netbox/project-static/styles/cable-trace.scss b/netbox/project-static/styles/cable-trace.scss index 85deafe96fe..51d94d38a7e 100644 --- a/netbox/project-static/styles/cable-trace.scss +++ b/netbox/project-static/styles/cable-trace.scss @@ -59,8 +59,13 @@ svg { stroke: var(--nbx-trace-cable-shadow); stroke-width: 7px; } + line.wireless-link { + stroke: var(--nbx-trace-attachment); + stroke-dasharray: 4px 12px; + stroke-linecap: round; + } line.attachment { stroke: var(--nbx-trace-attachment); - stroke-dasharray: 5px, 5px; + stroke-dasharray: 5px; } } From 01f791a44ee93b0342a204c5dffdf7a71c783267 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 16:40:12 -0400 Subject: [PATCH 13/28] Add WirelessLANGroup model --- netbox/netbox/navigation_menu.py | 3 +- netbox/templates/wireless/wirelesslan.html | 10 +++ .../templates/wireless/wirelesslangroup.html | 72 ++++++++++++++++++ netbox/wireless/api/nested_serializers.py | 11 +++ netbox/wireless/api/serializers.py | 15 +++- netbox/wireless/api/urls.py | 1 + netbox/wireless/api/views.py | 12 +++ netbox/wireless/filtersets.py | 18 ++++- netbox/wireless/forms/bulk_edit.py | 25 ++++++- netbox/wireless/forms/bulk_import.py | 25 ++++++- netbox/wireless/forms/filtersets.py | 30 +++++++- netbox/wireless/forms/models.py | 27 ++++++- netbox/wireless/graphql/schema.py | 6 ++ netbox/wireless/graphql/types.py | 9 +++ netbox/wireless/migrations/0001_wireless.py | 25 ++++++- netbox/wireless/models.py | 49 ++++++++++++- netbox/wireless/tables.py | 23 +++++- netbox/wireless/urls.py | 11 +++ netbox/wireless/views.py | 73 +++++++++++++++++++ 19 files changed, 429 insertions(+), 16 deletions(-) create mode 100644 netbox/templates/wireless/wirelesslangroup.html diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation_menu.py index b3e11f6cea3..7f64a2df8df 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation_menu.py @@ -168,6 +168,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): label='Connections', items=( get_model_item('dcim', 'cable', 'Cables', actions=['import']), + get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']), MenuItem( link='dcim:interface_connections_list', link_text='Interface Connections', @@ -196,7 +197,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')): label='Wireless', items=( get_model_item('wireless', 'wirelesslan', 'Wireless LANs'), - get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']), + get_model_item('wireless', 'wirelesslangroup', 'Wireless LAN Groups'), ), ), ), diff --git a/netbox/templates/wireless/wirelesslan.html b/netbox/templates/wireless/wirelesslan.html index f8fabf558eb..f2133cd5440 100644 --- a/netbox/templates/wireless/wirelesslan.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -13,6 +13,16 @@
Wireless LAN
+ + + + diff --git a/netbox/templates/wireless/wirelesslangroup.html b/netbox/templates/wireless/wirelesslangroup.html new file mode 100644 index 00000000000..170f72eff6b --- /dev/null +++ b/netbox/templates/wireless/wirelesslangroup.html @@ -0,0 +1,72 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} + +{% block breadcrumbs %} + {{ block.super }} + {% for group in object.get_ancestors %} + + {% endfor %} +{% endblock %} + +{% block content %} +
+
+
+
Wireless LAN Group
+
+
Status{{ object.get_status_display }}
SSID {{ object.ssid|placeholder }}SSID {{ object.ssid }}
Group + {% if object.group %} + {{ object.group }} + {% else %} + None + {% endif %} +
Description {{ object.description|placeholder }}
+ + + + + + + + + + + + + + + + +
Name{{ object.name }}
Description{{ object.description|placeholder }}
Parent + {% if object.parent %} + {{ object.parent }} + {% else %} + + {% endif %} +
Wireless LANs + {{ wirelesslans_table.rows|length }} +
+
+
+ {% plugin_left_page object %} +
+
+ {% include 'inc/custom_fields_panel.html' %} + {% plugin_right_page object %} +
+ +
+
+
+
Wireless LANs
+
+ {% include 'inc/table.html' with table=wirelesslans_table %} +
+ {% if perms.wireless.add_wirelesslan %} + + {% endif %} +
+ {% include 'inc/paginator.html' with paginator=wirelesslans_table.paginator page=wirelesslans_table.page %} + {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox/wireless/api/nested_serializers.py b/netbox/wireless/api/nested_serializers.py index 5a8cf66716d..e9a840bfc44 100644 --- a/netbox/wireless/api/nested_serializers.py +++ b/netbox/wireless/api/nested_serializers.py @@ -5,10 +5,21 @@ __all__ = ( 'NestedWirelessLANSerializer', + 'NestedWirelessLANGroupSerializer', 'NestedWirelessLinkSerializer', ) +class NestedWirelessLANGroupSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') + wirelesslan_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = WirelessLANGroup + fields = ['id', 'url', 'display', 'name', 'slug', 'wirelesslan_count', '_depth'] + + class NestedWirelessLANSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 5a733012926..24395b77c52 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -4,7 +4,7 @@ from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer from netbox.api import ChoiceField -from netbox.api.serializers import PrimaryModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from wireless.models import * from .nested_serializers import * @@ -14,6 +14,19 @@ ) +class WirelessLANGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') + parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True) + wirelesslan_count = serializers.IntegerField(read_only=True) + + class Meta: + model = WirelessLANGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', + 'wirelesslan_count', '_depth', + ] + + class WirelessLANSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') vlan = NestedVLANSerializer(required=False, allow_null=True) diff --git a/netbox/wireless/api/urls.py b/netbox/wireless/api/urls.py index 431bb05f812..54f764db68f 100644 --- a/netbox/wireless/api/urls.py +++ b/netbox/wireless/api/urls.py @@ -5,6 +5,7 @@ router = OrderedDefaultRouter() router.APIRootView = views.WirelessRootView +router.register('wireless-lan-groupss', views.WirelessLANGroupViewSet) router.register('wireless-lans', views.WirelessLANViewSet) router.register('wireless-links', views.WirelessLinkViewSet) diff --git a/netbox/wireless/api/views.py b/netbox/wireless/api/views.py index aa361a7f765..734f6940fde 100644 --- a/netbox/wireless/api/views.py +++ b/netbox/wireless/api/views.py @@ -14,6 +14,18 @@ def get_view_name(self): return 'Wireless' +class WirelessLANGroupViewSet(CustomFieldModelViewSet): + queryset = WirelessLANGroup.objects.add_related_count( + WirelessLANGroup.objects.all(), + WirelessLAN, + 'group', + 'wirelesslan_count', + cumulative=True + ) + serializer_class = serializers.WirelessLANGroupSerializer + filterset_class = filtersets.WirelessLANGroupFilterSet + + class WirelessLANViewSet(CustomFieldModelViewSet): queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags') serializer_class = serializers.WirelessLANSerializer diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index f765172dd68..ac503e474d3 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -3,15 +3,31 @@ from dcim.choices import LinkStatusChoices from extras.filters import TagFilter -from netbox.filtersets import PrimaryModelFilterSet +from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet from .models import * __all__ = ( 'WirelessLANFilterSet', + 'WirelessLANGroupFilterSet', 'WirelessLinkFilterSet', ) +class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): + parent_id = django_filters.ModelMultipleChoiceFilter( + queryset=WirelessLANGroup.objects.all() + ) + parent = django_filters.ModelMultipleChoiceFilter( + field_name='parent__slug', + queryset=WirelessLANGroup.objects.all(), + to_field_name='slug' + ) + + class Meta: + model = WirelessLANGroup + fields = ['id', 'name', 'slug', 'description'] + + class WirelessLANFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 72af81f566f..c0d5a925e3f 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -9,15 +9,38 @@ __all__ = ( 'WirelessLANBulkEditForm', + 'WirelessLANGroupBulkEditForm', 'WirelessLinkBulkEditForm', ) +class WirelessLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=WirelessLANGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + parent = DynamicModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['parent', 'description'] + + class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=WirelessLAN.objects.all(), widget=forms.MultipleHiddenInput ) + group = DynamicModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False + ) vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, @@ -31,7 +54,7 @@ class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode ) class Meta: - nullable_fields = ['vlan', 'ssid', 'description'] + nullable_fields = ['ssid', 'group', 'vlan', 'description'] class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 763305c38d8..6b22728f62f 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -2,16 +2,37 @@ from dcim.models import Interface from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN -from utilities.forms import CSVChoiceField, CSVModelChoiceField +from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField from wireless.models import * __all__ = ( 'WirelessLANCSVForm', + 'WirelessLANGroupCSVForm', 'WirelessLinkCSVForm', ) +class WirelessLANGroupCSVForm(CustomFieldModelCSVForm): + parent = CSVModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Parent group' + ) + slug = SlugField() + + class Meta: + model = WirelessLANGroup + fields = ('name', 'slug', 'parent', 'description') + + class WirelessLANCSVForm(CustomFieldModelCSVForm): + group = CSVModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned group' + ) vlan = CSVModelChoiceField( queryset=VLAN.objects.all(), to_field_name='name', @@ -20,7 +41,7 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm): class Meta: model = WirelessLAN - fields = ('ssid', 'description', 'vlan') + fields = ('ssid', 'group', 'description', 'vlan') class WirelessLinkCSVForm(CustomFieldModelCSVForm): diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 6da3cd716fd..13aae99a5f3 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -3,19 +3,38 @@ from dcim.choices import LinkStatusChoices from extras.forms import CustomFieldModelFilterForm -from utilities.forms import add_blank_choice, BootstrapMixin, StaticSelect, TagFilterField +from utilities.forms import ( + add_blank_choice, BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelect, TagFilterField, +) from wireless.models import * __all__ = ( 'WirelessLANFilterForm', + 'WirelessLANGroupFilterForm', 'WirelessLinkFilterForm', ) +class WirelessLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = WirelessLANGroup + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + parent_id = DynamicModelMultipleChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False, + label=_('Parent group'), + fetch_trigger='open' + ) + + class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = WirelessLAN field_groups = [ - ['q', 'tag'], + ('q', 'tag'), + ('group_id',), ] q = forms.CharField( required=False, @@ -26,6 +45,13 @@ class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm): required=False, label='SSID' ) + group_id = DynamicModelMultipleChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False, + null_option='None', + label=_('Group'), + fetch_trigger='open' + ) tag = TagFilterField(model) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 174eb5983f8..ca20b6ea882 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -2,16 +2,37 @@ from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN -from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect +from utilities.forms import ( + BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, StaticSelect, +) from wireless.models import * __all__ = ( 'WirelessLANForm', + 'WirelessLANGroupForm', 'WirelessLinkForm', ) +class WirelessLANGroupForm(BootstrapMixin, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False + ) + slug = SlugField() + + class Meta: + model = WirelessLANGroup + fields = [ + 'parent', 'name', 'slug', 'description', + ] + + class WirelessLANForm(BootstrapMixin, CustomFieldModelForm): + group = DynamicModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False + ) vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False @@ -24,10 +45,10 @@ class WirelessLANForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLAN fields = [ - 'ssid', 'description', 'vlan', 'tags', + 'ssid', 'group', 'description', 'vlan', 'tags', ] fieldsets = ( - ('Wireless LAN', ('ssid', 'description', 'tags')), + ('Wireless LAN', ('ssid', 'group', 'description', 'tags')), ('VLAN', ('vlan',)), ) diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py index 8297f454516..05fc57c4d31 100644 --- a/netbox/wireless/graphql/schema.py +++ b/netbox/wireless/graphql/schema.py @@ -7,3 +7,9 @@ class WirelessQuery(graphene.ObjectType): wirelesslan = ObjectField(WirelessLANType) wirelesslan_list = ObjectListField(WirelessLANType) + + wirelesslangroup = ObjectField(WirelessLANGroupType) + wirelesslangroup_list = ObjectListField(WirelessLANGroupType) + + wirelesslink = ObjectField(WirelessLinkType) + wirelesslink_list = ObjectListField(WirelessLinkType) diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 0afd8e69a12..4697cc44b59 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -3,10 +3,19 @@ __all__ = ( 'WirelessLANType', + 'WirelessLANGroupType', 'WirelessLinkType', ) +class WirelessLANGroupType(ObjectType): + + class Meta: + model = models.WirelessLANGroup + fields = '__all__' + filterset_class = filtersets.WirelessLANGroupFilterSet + + class WirelessLANType(ObjectType): class Meta: diff --git a/netbox/wireless/migrations/0001_wireless.py b/netbox/wireless/migrations/0001_wireless.py index 8eb042d7d31..068f4d64a45 100644 --- a/netbox/wireless/migrations/0001_wireless.py +++ b/netbox/wireless/migrations/0001_wireless.py @@ -1,8 +1,7 @@ -# Generated by Django 3.2.8 on 2021-10-13 13:44 - import django.core.serializers.json from django.db import migrations, models import django.db.models.deletion +import mptt.fields import taggit.managers @@ -17,6 +16,27 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='WirelessLANGroup', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='wireless.wirelesslangroup')), + ], + options={ + 'ordering': ('name', 'pk'), + 'unique_together': {('parent', 'name')}, + }, + ), migrations.CreateModel( name='WirelessLAN', fields=[ @@ -25,6 +45,7 @@ class Migration(migrations.Migration): ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), ('id', models.BigAutoField(primary_key=True, serialize=False)), ('ssid', models.CharField(max_length=32)), + ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wireless_lans', to='wireless.wirelesslangroup')), ('description', models.CharField(blank=True, max_length=200)), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')), diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index d02358f1cd8..12c4e55aa41 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -1,20 +1,58 @@ from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse +from mptt.models import MPTTModel, TreeForeignKey from dcim.choices import LinkStatusChoices from dcim.constants import WIRELESS_IFACE_TYPES from extras.utils import extras_features -from netbox.models import BigIDModel, PrimaryModel +from netbox.models import BigIDModel, NestedGroupModel, PrimaryModel from utilities.querysets import RestrictedQuerySet from .constants import SSID_MAX_LENGTH __all__ = ( 'WirelessLAN', + 'WirelessLANGroup', 'WirelessLink', ) +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +class WirelessLANGroup(NestedGroupModel): + """ + A nested grouping of WirelessLANs + """ + name = models.CharField( + max_length=100, + unique=True + ) + slug = models.SlugField( + max_length=100, + unique=True + ) + parent = TreeForeignKey( + to='self', + on_delete=models.CASCADE, + related_name='children', + blank=True, + null=True, + db_index=True + ) + description = models.CharField( + max_length=200, + blank=True + ) + + class Meta: + ordering = ('name', 'pk') + unique_together = ( + ('parent', 'name') + ) + + def get_absolute_url(self): + return reverse('wireless:wirelesslangroup', args=[self.pk]) + + @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class WirelessLAN(PrimaryModel): """ @@ -24,6 +62,13 @@ class WirelessLAN(PrimaryModel): max_length=SSID_MAX_LENGTH, verbose_name='SSID' ) + group = models.ForeignKey( + to='wireless.WirelessLANGroup', + on_delete=models.SET_NULL, + related_name='wireless_lans', + blank=True, + null=True + ) vlan = models.ForeignKey( to='ipam.VLAN', on_delete=models.PROTECT, @@ -100,7 +145,7 @@ class WirelessLink(PrimaryModel): objects = RestrictedQuerySet.as_manager() - clone_fields = ('ssid', 'status') + clone_fields = ('ssid', 'group', 'status') class Meta: ordering = ['pk'] diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 9b0ef729117..58d77b56f73 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -1,14 +1,35 @@ import django_tables2 as tables +from utilities.tables import ( + BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn, +) from .models import * -from utilities.tables import BaseTable, ChoiceFieldColumn, TagColumn, ToggleColumn __all__ = ( 'WirelessLANTable', + 'WirelessLANGroupTable', 'WirelessLinkTable', ) +class WirelessLANGroupTable(BaseTable): + pk = ToggleColumn() + name = MPTTColumn( + linkify=True + ) + wirelesslan_count = LinkedCountColumn( + viewname='wireless:wirelesslan_list', + url_params={'group_id': 'pk'}, + verbose_name='Wireless LANs' + ) + actions = ButtonsColumn(WirelessLANGroup) + + class Meta(BaseTable.Meta): + model = WirelessLANGroup + fields = ('pk', 'name', 'wirelesslan_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'wirelesslan_count', 'description', 'actions') + + class WirelessLANTable(BaseTable): pk = ToggleColumn() ssid = tables.Column( diff --git a/netbox/wireless/urls.py b/netbox/wireless/urls.py index 21d704e6a7c..684f55ad593 100644 --- a/netbox/wireless/urls.py +++ b/netbox/wireless/urls.py @@ -7,6 +7,17 @@ app_name = 'wireless' urlpatterns = ( + # Wireless LAN groups + path('wireless-lan-groups/', views.WirelessLANGroupListView.as_view(), name='wirelesslangroup_list'), + path('wireless-lan-groups/add/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_add'), + path('wireless-lan-groups/import/', views.WirelessLANGroupBulkImportView.as_view(), name='wirelesslangroup_import'), + path('wireless-lan-groups/edit/', views.WirelessLANGroupBulkEditView.as_view(), name='wirelesslangroup_bulk_edit'), + path('wireless-lan-groups/delete/', views.WirelessLANGroupBulkDeleteView.as_view(), name='wirelesslangroup_bulk_delete'), + path('wireless-lan-groups//', views.WirelessLANGroupView.as_view(), name='wirelesslangroup'), + path('wireless-lan-groups//edit/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_edit'), + path('wireless-lan-groups//delete/', views.WirelessLANGroupDeleteView.as_view(), name='wirelesslangroup_delete'), + path('wireless-lan-groups//changelog/', ObjectChangeLogView.as_view(), name='wirelesslangroup_changelog', kwargs={'model': WirelessLANGroup}), + # Wireless LANs path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'), path('wireless-lans/add/', views.WirelessLANEditView.as_view(), name='wirelesslan_add'), diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index 041ffbd420f..6405d46df23 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -1,8 +1,81 @@ from netbox.views import generic +from utilities.tables import paginate_table from . import filtersets, forms, tables from .models import * +# +# Wireless LAN groups +# + +class WirelessLANGroupListView(generic.ObjectListView): + queryset = WirelessLANGroup.objects.add_related_count( + WirelessLANGroup.objects.all(), + WirelessLAN, + 'group', + 'wirelesslan_count', + cumulative=True + ) + filterset = filtersets.WirelessLANGroupFilterSet + filterset_form = forms.WirelessLANGroupFilterForm + table = tables.WirelessLANGroupTable + + +class WirelessLANGroupView(generic.ObjectView): + queryset = WirelessLANGroup.objects.all() + + def get_extra_context(self, request, instance): + wirelesslans = WirelessLAN.objects.restrict(request.user, 'view').filter( + group=instance + ) + wirelesslans_table = tables.WirelessLANTable(wirelesslans, exclude=('group',)) + paginate_table(wirelesslans_table, request) + + return { + 'wirelesslans_table': wirelesslans_table, + } + + +class WirelessLANGroupEditView(generic.ObjectEditView): + queryset = WirelessLANGroup.objects.all() + model_form = forms.WirelessLANGroupForm + + +class WirelessLANGroupDeleteView(generic.ObjectDeleteView): + queryset = WirelessLANGroup.objects.all() + + +class WirelessLANGroupBulkImportView(generic.BulkImportView): + queryset = WirelessLANGroup.objects.all() + model_form = forms.WirelessLANGroupCSVForm + table = tables.WirelessLANGroupTable + + +class WirelessLANGroupBulkEditView(generic.BulkEditView): + queryset = WirelessLANGroup.objects.add_related_count( + WirelessLANGroup.objects.all(), + WirelessLAN, + 'group', + 'wirelesslan_count', + cumulative=True + ) + filterset = filtersets.WirelessLANGroupFilterSet + table = tables.WirelessLANGroupTable + form = forms.WirelessLANGroupBulkEditForm + + +class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView): + queryset = WirelessLANGroup.objects.add_related_count( + WirelessLANGroup.objects.all(), + WirelessLAN, + 'group', + 'wirelesslan_count', + cumulative=True + ) + filterset = filtersets.WirelessLANGroupFilterSet + table = tables.WirelessLANGroupTable + + # # Wireless LANs # From 438b4b47581f2b47a19243f3e848aabe52830b38 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 20:16:36 -0400 Subject: [PATCH 14/28] Add rf_role to Interface --- netbox/dcim/api/serializers.py | 3 ++- netbox/dcim/choices.py | 10 ++++++++ netbox/dcim/filtersets.py | 4 ++-- netbox/dcim/forms/bulk_edit.py | 2 +- netbox/dcim/forms/bulk_import.py | 7 +++++- netbox/dcim/forms/filtersets.py | 14 ++++++++--- netbox/dcim/forms/models.py | 5 ++-- netbox/dcim/forms/object_create.py | 10 ++++++-- netbox/dcim/graphql/types.py | 3 +++ netbox/dcim/migrations/0136_wireless.py | 7 ++++-- netbox/dcim/models/device_components.py | 12 ++++++++-- netbox/dcim/tables/devices.py | 9 +++---- netbox/templates/dcim/interface.html | 29 +++++++++++++++-------- netbox/templates/dcim/interface_edit.html | 1 + 14 files changed, 86 insertions(+), 30 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 03d14a160d4..af1b6eeb0da 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -632,6 +632,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con parent = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) + rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True) rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False) rf_channel_width = ChoiceField(choices=WirelessChannelWidthChoices, required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) @@ -648,7 +649,7 @@ class Meta: model = Interface fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'wwn', 'mgmt_only', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'untagged_vlan', + 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied', diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index d58d5466d01..b0ebf7cf443 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1139,6 +1139,16 @@ class CableLengthUnitChoices(ChoiceSet): # Wireless # +class WirelessRoleChoices(ChoiceSet): + ROLE_AP = 'ap' + ROLE_STATION = 'station' + + CHOICES = ( + (ROLE_AP, 'Access point'), + (ROLE_STATION, 'Station'), + ) + + class WirelessChannelChoices(ChoiceSet): CHANNEL_AUTO = 'auto' diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index f778fd083c9..ab4336dbf05 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -991,8 +991,8 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT class Meta: model = Interface fields = [ - 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_channel', 'rf_channel_width', - 'description', + 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel', + 'rf_channel_width', 'description', ] def filter_device(self, queryset, name, value): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index f710e7d8cff..382a0570eb7 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -926,7 +926,7 @@ def __init__(self, *args, **kwargs): class InterfaceBulkEditForm( form_from_model(Interface, [ 'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', - 'mode', 'rf_channel', 'rf_channel_width', + 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', ]), BootstrapMixin, AddRemoveTagsForm, diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 95b4dd136cb..675e850ed61 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -579,12 +579,17 @@ class InterfaceCSVForm(CustomFieldModelCSVForm): required=False, help_text='IEEE 802.1Q operational mode (for L2 interfaces)' ) + rf_role = CSVChoiceField( + choices=WirelessRoleChoices, + required=False, + help_text='Wireless role (AP/station)' + ) class Meta: model = Interface fields = ( 'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn', - 'mtu', 'mgmt_only', 'description', 'mode', 'rf_channel', 'rf_channel_width', + 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 169b93028ec..a6f25ae002b 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -963,7 +963,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'], - ['rf_channel', 'rf_channel_width'], + ['rf_role', 'rf_channel', 'rf_channel_width'], ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( @@ -991,15 +991,23 @@ class InterfaceFilterForm(DeviceComponentFilterForm): required=False, label='WWN' ) + rf_role = forms.MultipleChoiceField( + choices=WirelessRoleChoices, + required=False, + widget=StaticSelectMultiple(), + label='Wireless role' + ) rf_channel = forms.MultipleChoiceField( choices=WirelessChannelChoices, required=False, - widget=StaticSelectMultiple() + widget=StaticSelectMultiple(), + label='Wireless channel' ) rf_channel_width = forms.MultipleChoiceField( choices=WirelessChannelWidthChoices, required=False, - widget=StaticSelectMultiple() + widget=StaticSelectMultiple(), + label='Channel width' ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index cd697e9f3e4..4cfa40fe2d6 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1104,13 +1104,14 @@ class Meta: model = Interface fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', - 'mark_connected', 'description', 'mode', 'rf_channel', 'rf_channel_width', 'wireless_lans', 'untagged_vlan', - 'tagged_vlans', 'tags', + 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', 'wireless_lans', + 'untagged_vlan', 'tagged_vlans', 'tags', ] widgets = { 'device': forms.HiddenInput(), 'type': StaticSelect(), 'mode': StaticSelect(), + 'rf_role': StaticSelect(), 'rf_channel': StaticSelect(), 'rf_channel_width': StaticSelect(), } diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index db28412e6ce..3998dcbc1ed 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -467,6 +467,12 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): required=False, widget=StaticSelect() ) + rf_role = forms.ChoiceField( + choices=add_blank_choice(WirelessRoleChoices), + required=False, + widget=StaticSelect(), + label='Wireless role' + ) rf_channel = forms.ChoiceField( choices=add_blank_choice(WirelessChannelChoices), required=False, @@ -489,8 +495,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): ) field_order = ( 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'description', 'mgmt_only', 'mark_connected', 'rf_channel', 'rf_channel_width', 'mode' 'untagged_vlan', - 'tagged_vlans', 'tags' + 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_width', 'mode', + 'untagged_vlan', 'tagged_vlans', 'tags' ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 55f1ba15049..3d489973ced 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -206,6 +206,9 @@ class Meta: def resolve_mode(self, info): return self.mode or None + def resolve_rf_role(self, info): + return self.rf_role or None + def resolve_rf_channel(self, info): return self.rf_channel or None diff --git a/netbox/dcim/migrations/0136_wireless.py b/netbox/dcim/migrations/0136_wireless.py index 3b33f7d3f0c..7a3ee967397 100644 --- a/netbox/dcim/migrations/0136_wireless.py +++ b/netbox/dcim/migrations/0136_wireless.py @@ -1,5 +1,3 @@ -# Generated by Django 3.2.8 on 2021-10-13 13:44 - from django.db import migrations, models @@ -10,6 +8,11 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name='interface', + name='rf_role', + field=models.CharField(blank=True, max_length=30), + ), migrations.AddField( model_name='interface', name='rf_channel', diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f8649b419ac..74724baf80d 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -524,6 +524,12 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint): verbose_name='WWN', help_text='64-bit World Wide Name' ) + rf_role = models.CharField( + max_length=30, + choices=WirelessRoleChoices, + blank=True, + verbose_name='Wireless role' + ) rf_channel = models.CharField( max_length=50, choices=WirelessChannelChoices, @@ -636,9 +642,11 @@ def clean(self): raise ValidationError({'lag': "A LAG interface cannot be its own parent."}) # RF channel attributes may be set only for wireless interfaces - if self.rf_channel and self.type not in WIRELESS_IFACE_TYPES: + if self.rf_role and not self.is_wireless: + raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."}) + if self.rf_channel and not self.is_wireless: raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."}) - if self.rf_channel_width and self.type not in WIRELESS_IFACE_TYPES: + if self.rf_channel_width and not self.is_wireless: raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."}) # Validate untagged VLAN diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index a375a77cc9d..7f2ff1c71ef 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -496,8 +496,8 @@ class Meta(DeviceComponentTable.Meta): model = Interface fields = ( 'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', - 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', + 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', + 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') @@ -528,8 +528,9 @@ class Meta(DeviceComponentTable.Meta): model = Interface fields = ( 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'link_peer', 'connection', 'tags', - 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', + 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', + 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', + 'actions', ) order_by = ('name',) default_columns = ( diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index bf24a89ef02..90c9497ef96 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -39,16 +39,6 @@
Type {{ object.get_type_display }} - {% if object.is_wireless %} - - Channel - {{ object.get_rf_channel_display|placeholder }} - - - Channel Width - {{ object.get_rf_channel_width_display|placeholder }} - - {% endif %} Enabled @@ -274,6 +264,25 @@
{% endif %} {% if object.is_wireless %} +
+
Wireless
+
+ + + + + + + + + + + + + +
Role{{ object.get_rf_role_display|placeholder }}
Channel{{ object.get_rf_channel_display|placeholder }}
Channel Width{{ object.get_rf_channel_width_display|placeholder }}
+
+
Wireless LANs
diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index 51834f4e2f8..cb8d5182818 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -34,6 +34,7 @@
Interface
Wireless
+ {% render_field form.rf_role %} {% render_field form.rf_channel %} {% render_field form.rf_channel_width %} {% render_field form.wireless_lans %} From 4c475c1b33018be900393a74082d381eb23d0429 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 13 Oct 2021 20:56:14 -0400 Subject: [PATCH 15/28] Extend wireless channel choices --- netbox/dcim/choices.py | 108 +++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index b0ebf7cf443..18537812c70 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1200,6 +1200,28 @@ class WirelessChannelChoices(ChoiceSet): CHANNEL_5G_124 = '5g-124' CHANNEL_5G_126 = '5g-126' CHANNEL_5G_128 = '5g-128' + CHANNEL_5G_132 = '5g-132' + CHANNEL_5G_134 = '5g-134' + CHANNEL_5G_136 = '5g-136' + CHANNEL_5G_138 = '5g-138' + CHANNEL_5G_140 = '5g-140' + CHANNEL_5G_142 = '5g-142' + CHANNEL_5G_144 = '5g-144' + CHANNEL_5G_149 = '5g-149' + CHANNEL_5G_151 = '5g-151' + CHANNEL_5G_153 = '5g-153' + CHANNEL_5G_155 = '5g-155' + CHANNEL_5G_157 = '5g-157' + CHANNEL_5G_159 = '5g-159' + CHANNEL_5G_161 = '5g-161' + CHANNEL_5G_163 = '5g-163' + CHANNEL_5G_165 = '5g-165' + CHANNEL_5G_167 = '5g-167' + CHANNEL_5G_169 = '5g-169' + CHANNEL_5G_171 = '5g-171' + CHANNEL_5G_173 = '5g-173' + CHANNEL_5G_175 = '5g-175' + CHANNEL_5G_177 = '5g-177' CHOICES = ( (CHANNEL_AUTO, 'Auto'), @@ -1224,38 +1246,60 @@ class WirelessChannelChoices(ChoiceSet): ( '5 GHz (802.11a/n/ac/ax)', ( - (CHANNEL_5G_32, '32 (5160 MHz)'), - (CHANNEL_5G_34, '34 (5170 MHz)'), - (CHANNEL_5G_36, '36 (5180 MHz)'), - (CHANNEL_5G_38, '38 (5190 MHz)'), - (CHANNEL_5G_40, '40 (5200 MHz)'), - (CHANNEL_5G_42, '42 (5210 MHz)'), - (CHANNEL_5G_44, '44 (5220 MHz)'), - (CHANNEL_5G_46, '46 (5230 MHz)'), - (CHANNEL_5G_48, '48 (5240 MHz)'), - (CHANNEL_5G_50, '50 (5250 MHz)'), - (CHANNEL_5G_52, '52 (5260 MHz)'), - (CHANNEL_5G_54, '54 (5270 MHz)'), - (CHANNEL_5G_56, '56 (5280 MHz)'), - (CHANNEL_5G_58, '58 (5290 MHz)'), - (CHANNEL_5G_60, '60 (5300 MHz)'), - (CHANNEL_5G_62, '62 (5310 MHz)'), - (CHANNEL_5G_64, '64 (5320 MHz)'), - (CHANNEL_5G_100, '100 (5500 MHz)'), - (CHANNEL_5G_102, '102 (5510 MHz)'), - (CHANNEL_5G_104, '104 (5520 MHz)'), - (CHANNEL_5G_106, '106 (5530 MHz)'), - (CHANNEL_5G_108, '108 (5540 MHz)'), - (CHANNEL_5G_110, '110 (5550 MHz)'), - (CHANNEL_5G_112, '112 (5560 MHz)'), - (CHANNEL_5G_114, '114 (5570 MHz)'), - (CHANNEL_5G_116, '116 (5580 MHz)'), - (CHANNEL_5G_118, '118 (5590 MHz)'), - (CHANNEL_5G_120, '120 (5600 MHz)'), - (CHANNEL_5G_122, '122 (5610 MHz)'), - (CHANNEL_5G_124, '124 (5620 MHz)'), - (CHANNEL_5G_126, '126 (5630 MHz)'), - (CHANNEL_5G_128, '128 (5640 MHz)'), + (CHANNEL_5G_32, '32 (5160/20 MHz)'), + (CHANNEL_5G_34, '34 (5170/40 MHz)'), + (CHANNEL_5G_36, '36 (5180/20 MHz)'), + (CHANNEL_5G_38, '38 (5190/40 MHz)'), + (CHANNEL_5G_40, '40 (5200/20 MHz)'), + (CHANNEL_5G_42, '42 (5210/80 MHz)'), + (CHANNEL_5G_44, '44 (5220/20 MHz)'), + (CHANNEL_5G_46, '46 (5230/40 MHz)'), + (CHANNEL_5G_48, '48 (5240/20 MHz)'), + (CHANNEL_5G_50, '50 (5250/160 MHz)'), + (CHANNEL_5G_52, '52 (5260/20 MHz)'), + (CHANNEL_5G_54, '54 (5270/40 MHz)'), + (CHANNEL_5G_56, '56 (5280/20 MHz)'), + (CHANNEL_5G_58, '58 (5290/80 MHz)'), + (CHANNEL_5G_60, '60 (5300/20 MHz)'), + (CHANNEL_5G_62, '62 (5310/40 MHz)'), + (CHANNEL_5G_64, '64 (5320/20 MHz)'), + (CHANNEL_5G_100, '100 (5500/20 MHz)'), + (CHANNEL_5G_102, '102 (5510/40 MHz)'), + (CHANNEL_5G_104, '104 (5520/20 MHz)'), + (CHANNEL_5G_106, '106 (5530/80 MHz)'), + (CHANNEL_5G_108, '108 (5540/20 MHz)'), + (CHANNEL_5G_110, '110 (5550/40 MHz)'), + (CHANNEL_5G_112, '112 (5560/20 MHz)'), + (CHANNEL_5G_114, '114 (5570/160 MHz)'), + (CHANNEL_5G_116, '116 (5580/20 MHz)'), + (CHANNEL_5G_118, '118 (5590/40 MHz)'), + (CHANNEL_5G_120, '120 (5600/20 MHz)'), + (CHANNEL_5G_122, '122 (5610/80 MHz)'), + (CHANNEL_5G_124, '124 (5620/20 MHz)'), + (CHANNEL_5G_126, '126 (5630/40 MHz)'), + (CHANNEL_5G_128, '128 (5640/20 MHz)'), + (CHANNEL_5G_132, '132 (5660/20 MHz)'), + (CHANNEL_5G_134, '134 (5670/40 MHz)'), + (CHANNEL_5G_136, '136 (5680/20 MHz)'), + (CHANNEL_5G_138, '138 (5690/80 MHz)'), + (CHANNEL_5G_140, '140 (5700/20 MHz)'), + (CHANNEL_5G_142, '142 (5710/40 MHz)'), + (CHANNEL_5G_144, '144 (5720/20 MHz)'), + (CHANNEL_5G_149, '149 (5745/20 MHz)'), + (CHANNEL_5G_151, '151 (5755/40 MHz)'), + (CHANNEL_5G_153, '153 (5765/20 MHz)'), + (CHANNEL_5G_155, '155 (5775/80 MHz)'), + (CHANNEL_5G_157, '157 (5785/20 MHz)'), + (CHANNEL_5G_159, '159 (5795/40 MHz)'), + (CHANNEL_5G_161, '161 (5805/20 MHz)'), + (CHANNEL_5G_163, '163 (5815/160 MHz)'), + (CHANNEL_5G_165, '165 (5825/20 MHz)'), + (CHANNEL_5G_167, '167 (5835/40 MHz)'), + (CHANNEL_5G_169, '169 (5845/20 MHz)'), + (CHANNEL_5G_171, '171 (5855/80 MHz)'), + (CHANNEL_5G_173, '173 (5865/20 MHz)'), + (CHANNEL_5G_175, '175 (5875/40 MHz)'), + (CHANNEL_5G_177, '177 (5885/20 MHz)'), ) ), ) From bdf359470ea749f46a98ffb7f163a1b04996e0f5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 09:48:12 -0400 Subject: [PATCH 16/28] Include WirelessLAN attached interfaces --- netbox/templates/wireless/wirelesslan.html | 11 +++++++-- netbox/wireless/models.py | 2 +- netbox/wireless/tables.py | 26 ++++++++++++++++++++-- netbox/wireless/views.py | 17 +++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/netbox/templates/wireless/wirelesslan.html b/netbox/templates/wireless/wirelesslan.html index f2133cd5440..cfe13ca45e4 100644 --- a/netbox/templates/wireless/wirelesslan.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -49,8 +49,15 @@
Wireless LAN
-
- {% plugin_full_width_page object %} +
+
+
Attached Interfaces
+
+ {% include 'inc/table.html' with table=interfaces_table %} +
+ {% include 'inc/paginator.html' with paginator=interfaces_table.paginator page=interfaces_table.page %} + {% plugin_full_width_page object %} +
{% endblock %} diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 12c4e55aa41..b0cacde15ef 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -145,7 +145,7 @@ class WirelessLink(PrimaryModel): objects = RestrictedQuerySet.as_manager() - clone_fields = ('ssid', 'group', 'status') + clone_fields = ('ssid', 'status') class Meta: ordering = ['pk'] diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 58d77b56f73..671e948d2ab 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -1,5 +1,6 @@ import django_tables2 as tables +from dcim.models import Interface from utilities.tables import ( BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn, ) @@ -35,14 +36,35 @@ class WirelessLANTable(BaseTable): ssid = tables.Column( linkify=True ) + group = tables.Column( + linkify=True + ) + interface_count = tables.Column( + verbose_name='Interfaces' + ) tags = TagColumn( url_name='wireless:wirelesslan_list' ) class Meta(BaseTable.Meta): model = WirelessLAN - fields = ('pk', 'ssid', 'description', 'vlan') - default_columns = ('pk', 'ssid', 'description', 'vlan') + fields = ('pk', 'ssid', 'group', 'description', 'vlan', 'interface_count', 'tags') + default_columns = ('pk', 'ssid', 'group', 'description', 'vlan', 'interface_count') + + +class WirelessLANInterfacesTable(BaseTable): + pk = ToggleColumn() + device = tables.Column( + linkify=True + ) + name = tables.Column( + linkify=True + ) + + class Meta(BaseTable.Meta): + model = Interface + fields = ('pk', 'device', 'name', 'rf_role', 'rf_channel') + default_columns = ('pk', 'device', 'name', 'rf_role', 'rf_channel') class WirelessLinkTable(BaseTable): diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index 6405d46df23..a9238df33de 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -1,5 +1,7 @@ +from dcim.models import Interface from netbox.views import generic from utilities.tables import paginate_table +from utilities.utils import count_related from . import filtersets, forms, tables from .models import * @@ -81,7 +83,9 @@ class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView): # class WirelessLANListView(generic.ObjectListView): - queryset = WirelessLAN.objects.all() + queryset = WirelessLAN.objects.annotate( + interface_count=count_related(Interface, 'wireless_lans') + ) filterset = filtersets.WirelessLANFilterSet filterset_form = forms.WirelessLANFilterForm table = tables.WirelessLANTable @@ -90,6 +94,17 @@ class WirelessLANListView(generic.ObjectListView): class WirelessLANView(generic.ObjectView): queryset = WirelessLAN.objects.all() + def get_extra_context(self, request, instance): + attached_interfaces = Interface.objects.restrict(request.user, 'view').filter( + wireless_lans=instance + ) + interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces) + paginate_table(interfaces_table, request) + + return { + 'interfaces_table': interfaces_table, + } + class WirelessLANEditView(generic.ObjectEditView): queryset = WirelessLAN.objects.all() From fb9da87abb8462c811391bf5bba530394f9b9f25 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 10:02:05 -0400 Subject: [PATCH 17/28] Add devices to WirelessLinkForm --- netbox/dcim/models/device_components.py | 4 ++++ netbox/wireless/forms/models.py | 20 ++++++++++++++++---- netbox/wireless/tables.py | 14 ++++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 74724baf80d..39c618f4d53 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -656,6 +656,10 @@ def clean(self): "device, or it must be global".format(self.untagged_vlan) }) + @property + def _occupied(self): + return super()._occupied or bool(self.wireless_link_id) + @property def is_wired(self): return not self.is_virtual and not self.is_wireless diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index ca20b6ea882..a3454c79a39 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -1,4 +1,4 @@ -from dcim.models import Interface +from dcim.models import Device, Interface from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN @@ -54,18 +54,30 @@ class Meta: class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): + device_a = DynamicModelChoiceField( + queryset=Device.objects.all(), + label='Device A' + ) interface_a = DynamicModelChoiceField( queryset=Interface.objects.all(), query_params={ - 'kind': 'wireless' + 'kind': 'wireless', + 'device_id': '$device_a', }, + disabled_indicator='_occupied', label='Interface A' ) + device_b = DynamicModelChoiceField( + queryset=Device.objects.all(), + label='Device B' + ) interface_b = DynamicModelChoiceField( queryset=Interface.objects.all(), query_params={ - 'kind': 'wireless' + 'kind': 'wireless', + 'device_id': '$device_b', }, + disabled_indicator='_occupied', label='Interface B' ) tags = DynamicModelMultipleChoiceField( @@ -76,7 +88,7 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLink fields = [ - 'interface_a', 'interface_b', 'status', 'ssid', 'description', 'tags', + 'device_a', 'interface_a', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'tags', ] widgets = { 'status': StaticSelect, diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 671e948d2ab..486fa2a7126 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -74,9 +74,17 @@ class WirelessLinkTable(BaseTable): verbose_name='ID' ) status = ChoiceFieldColumn() + device_a = tables.Column( + accessor=tables.A('interface_a__device'), + linkify=True + ) interface_a = tables.Column( linkify=True ) + device_b = tables.Column( + accessor=tables.A('interface_b__device'), + linkify=True + ) interface_b = tables.Column( linkify=True ) @@ -86,5 +94,7 @@ class WirelessLinkTable(BaseTable): class Meta(BaseTable.Meta): model = WirelessLink - fields = ('pk', 'id', 'status', 'interface_a', 'interface_b', 'ssid', 'description') - default_columns = ('pk', 'id', 'status', 'interface_a', 'interface_b', 'ssid', 'description') + fields = ('pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'description') + default_columns = ( + 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'description', + ) From 909b83c537cb08782802af17759f83c26f8237c5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 10:06:46 -0400 Subject: [PATCH 18/28] Include interface RF attributes on wireless link view --- .../wireless/inc/wirelesslink_interface.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/netbox/templates/wireless/inc/wirelesslink_interface.html b/netbox/templates/wireless/inc/wirelesslink_interface.html index 9c4669ad1c7..82f7cfd8d0d 100644 --- a/netbox/templates/wireless/inc/wirelesslink_interface.html +++ b/netbox/templates/wireless/inc/wirelesslink_interface.html @@ -1,3 +1,5 @@ +{% load helpers %} + @@ -17,4 +19,16 @@ {{ interface.get_type_display }} + + + + + + + +
Device
Role + {{ interface.get_rf_role_display|placeholder }} +
Channel + {{ interface.get_rf_channel_display|placeholder }} +
From 64dad7dbd282b8576e9211b5ae3e7ba4d78e6d06 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 15:11:03 -0400 Subject: [PATCH 19/28] Optimize migrations --- .../migrations/0003_rename_cable_peer.py | 2 -- ...able_peer.py => 0136_rename_cable_peer.py} | 4 +-- netbox/dcim/migrations/0136_wireless.py | 26 ------------------- netbox/dcim/migrations/0137_wireless.py | 25 +++++++++++++++--- .../0138_interface_wireless_link.py | 20 -------------- netbox/wireless/migrations/0001_wireless.py | 2 +- 6 files changed, 24 insertions(+), 55 deletions(-) rename netbox/dcim/migrations/{0139_rename_cable_peer.py => 0136_rename_cable_peer.py} (96%) delete mode 100644 netbox/dcim/migrations/0136_wireless.py delete mode 100644 netbox/dcim/migrations/0138_interface_wireless_link.py diff --git a/netbox/circuits/migrations/0003_rename_cable_peer.py b/netbox/circuits/migrations/0003_rename_cable_peer.py index 475a84d0f81..63dc1006e52 100644 --- a/netbox/circuits/migrations/0003_rename_cable_peer.py +++ b/netbox/circuits/migrations/0003_rename_cable_peer.py @@ -1,5 +1,3 @@ -# Generated by Django 3.2.8 on 2021-10-13 17:47 - from django.db import migrations diff --git a/netbox/dcim/migrations/0139_rename_cable_peer.py b/netbox/dcim/migrations/0136_rename_cable_peer.py similarity index 96% rename from netbox/dcim/migrations/0139_rename_cable_peer.py rename to netbox/dcim/migrations/0136_rename_cable_peer.py index 62a4bacdd0a..6958458e88a 100644 --- a/netbox/dcim/migrations/0139_rename_cable_peer.py +++ b/netbox/dcim/migrations/0136_rename_cable_peer.py @@ -1,12 +1,10 @@ -# Generated by Django 3.2.8 on 2021-10-13 17:47 - from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('dcim', '0138_interface_wireless_link'), + ('dcim', '0135_location_tenant'), ] operations = [ diff --git a/netbox/dcim/migrations/0136_wireless.py b/netbox/dcim/migrations/0136_wireless.py deleted file mode 100644 index 7a3ee967397..00000000000 --- a/netbox/dcim/migrations/0136_wireless.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0135_location_tenant'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='rf_role', - field=models.CharField(blank=True, max_length=30), - ), - migrations.AddField( - model_name='interface', - name='rf_channel', - field=models.CharField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='interface', - name='rf_channel_width', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - ] diff --git a/netbox/dcim/migrations/0137_wireless.py b/netbox/dcim/migrations/0137_wireless.py index 9108735a1a5..788157c23ee 100644 --- a/netbox/dcim/migrations/0137_wireless.py +++ b/netbox/dcim/migrations/0137_wireless.py @@ -1,19 +1,38 @@ -# Generated by Django 3.2.8 on 2021-10-13 13:44 - from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('dcim', '0136_wireless'), + ('dcim', '0136_rename_cable_peer'), ('wireless', '0001_wireless'), ] operations = [ + migrations.AddField( + model_name='interface', + name='rf_role', + field=models.CharField(blank=True, max_length=30), + ), + migrations.AddField( + model_name='interface', + name='rf_channel', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='interface', + name='rf_channel_width', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), migrations.AddField( model_name='interface', name='wireless_lans', field=models.ManyToManyField(blank=True, related_name='interfaces', to='wireless.WirelessLAN'), ), + migrations.AddField( + model_name='interface', + name='wireless_link', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wireless.wirelesslink'), + ), ] diff --git a/netbox/dcim/migrations/0138_interface_wireless_link.py b/netbox/dcim/migrations/0138_interface_wireless_link.py deleted file mode 100644 index 42b7a10422a..00000000000 --- a/netbox/dcim/migrations/0138_interface_wireless_link.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-13 15:29 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('wireless', '0001_wireless'), - ('dcim', '0137_wireless'), - ] - - operations = [ - migrations.AddField( - model_name='interface', - name='wireless_link', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wireless.wirelesslink'), - ), - ] diff --git a/netbox/wireless/migrations/0001_wireless.py b/netbox/wireless/migrations/0001_wireless.py index 068f4d64a45..9adc8757bad 100644 --- a/netbox/wireless/migrations/0001_wireless.py +++ b/netbox/wireless/migrations/0001_wireless.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dcim', '0136_wireless'), + ('dcim', '0136_rename_cable_peer'), ('extras', '0062_clear_secrets_changelog'), ('ipam', '0050_iprange'), ] From 01d3c062f210bcb59bc126a19dfb41a3ca421348 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Oct 2021 10:00:03 -0400 Subject: [PATCH 20/28] Move wireless field choices to wireless app --- netbox/dcim/api/serializers.py | 1 + netbox/dcim/choices.py | 185 ------------------------ netbox/dcim/forms/bulk_import.py | 1 + netbox/dcim/forms/filtersets.py | 1 + netbox/dcim/forms/object_create.py | 1 + netbox/dcim/models/device_components.py | 1 + netbox/wireless/choices.py | 182 +++++++++++++++++++++++ 7 files changed, 187 insertions(+), 185 deletions(-) create mode 100644 netbox/wireless/choices.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index f5073316365..d7857d5e8c9 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -19,6 +19,7 @@ from users.api.nested_serializers import NestedUserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedClusterSerializer +from wireless.choices import * from .nested_serializers import * diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index ae713d687e8..9f87dded081 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1154,191 +1154,6 @@ class CableLengthUnitChoices(ChoiceSet): ) -# -# Wireless -# - -class WirelessRoleChoices(ChoiceSet): - ROLE_AP = 'ap' - ROLE_STATION = 'station' - - CHOICES = ( - (ROLE_AP, 'Access point'), - (ROLE_STATION, 'Station'), - ) - - -class WirelessChannelChoices(ChoiceSet): - CHANNEL_AUTO = 'auto' - - # 2.4 GHz - CHANNEL_24G_1 = '2.4g-1' - CHANNEL_24G_2 = '2.4g-2' - CHANNEL_24G_3 = '2.4g-3' - CHANNEL_24G_4 = '2.4g-4' - CHANNEL_24G_5 = '2.4g-5' - CHANNEL_24G_6 = '2.4g-6' - CHANNEL_24G_7 = '2.4g-7' - CHANNEL_24G_8 = '2.4g-8' - CHANNEL_24G_9 = '2.4g-9' - CHANNEL_24G_10 = '2.4g-10' - CHANNEL_24G_11 = '2.4g-11' - CHANNEL_24G_12 = '2.4g-12' - CHANNEL_24G_13 = '2.4g-13' - - # 5 GHz - CHANNEL_5G_32 = '5g-32' - CHANNEL_5G_34 = '5g-34' - CHANNEL_5G_36 = '5g-36' - CHANNEL_5G_38 = '5g-38' - CHANNEL_5G_40 = '5g-40' - CHANNEL_5G_42 = '5g-42' - CHANNEL_5G_44 = '5g-44' - CHANNEL_5G_46 = '5g-46' - CHANNEL_5G_48 = '5g-48' - CHANNEL_5G_50 = '5g-50' - CHANNEL_5G_52 = '5g-52' - CHANNEL_5G_54 = '5g-54' - CHANNEL_5G_56 = '5g-56' - CHANNEL_5G_58 = '5g-58' - CHANNEL_5G_60 = '5g-60' - CHANNEL_5G_62 = '5g-62' - CHANNEL_5G_64 = '5g-64' - CHANNEL_5G_100 = '5g-100' - CHANNEL_5G_102 = '5g-102' - CHANNEL_5G_104 = '5g-104' - CHANNEL_5G_106 = '5g-106' - CHANNEL_5G_108 = '5g-108' - CHANNEL_5G_110 = '5g-110' - CHANNEL_5G_112 = '5g-112' - CHANNEL_5G_114 = '5g-114' - CHANNEL_5G_116 = '5g-116' - CHANNEL_5G_118 = '5g-118' - CHANNEL_5G_120 = '5g-120' - CHANNEL_5G_122 = '5g-122' - CHANNEL_5G_124 = '5g-124' - CHANNEL_5G_126 = '5g-126' - CHANNEL_5G_128 = '5g-128' - CHANNEL_5G_132 = '5g-132' - CHANNEL_5G_134 = '5g-134' - CHANNEL_5G_136 = '5g-136' - CHANNEL_5G_138 = '5g-138' - CHANNEL_5G_140 = '5g-140' - CHANNEL_5G_142 = '5g-142' - CHANNEL_5G_144 = '5g-144' - CHANNEL_5G_149 = '5g-149' - CHANNEL_5G_151 = '5g-151' - CHANNEL_5G_153 = '5g-153' - CHANNEL_5G_155 = '5g-155' - CHANNEL_5G_157 = '5g-157' - CHANNEL_5G_159 = '5g-159' - CHANNEL_5G_161 = '5g-161' - CHANNEL_5G_163 = '5g-163' - CHANNEL_5G_165 = '5g-165' - CHANNEL_5G_167 = '5g-167' - CHANNEL_5G_169 = '5g-169' - CHANNEL_5G_171 = '5g-171' - CHANNEL_5G_173 = '5g-173' - CHANNEL_5G_175 = '5g-175' - CHANNEL_5G_177 = '5g-177' - - CHOICES = ( - (CHANNEL_AUTO, 'Auto'), - ( - '2.4 GHz (802.11b/g/n/ax)', - ( - (CHANNEL_24G_1, '1 (2412 MHz)'), - (CHANNEL_24G_2, '2 (2417 MHz)'), - (CHANNEL_24G_3, '3 (2422 MHz)'), - (CHANNEL_24G_4, '4 (2427 MHz)'), - (CHANNEL_24G_5, '5 (2432 MHz)'), - (CHANNEL_24G_6, '6 (2437 MHz)'), - (CHANNEL_24G_7, '7 (2442 MHz)'), - (CHANNEL_24G_8, '8 (2447 MHz)'), - (CHANNEL_24G_9, '9 (2452 MHz)'), - (CHANNEL_24G_10, '10 (2457 MHz)'), - (CHANNEL_24G_11, '11 (2462 MHz)'), - (CHANNEL_24G_12, '12 (2467 MHz)'), - (CHANNEL_24G_13, '13 (2472 MHz)'), - ) - ), - ( - '5 GHz (802.11a/n/ac/ax)', - ( - (CHANNEL_5G_32, '32 (5160/20 MHz)'), - (CHANNEL_5G_34, '34 (5170/40 MHz)'), - (CHANNEL_5G_36, '36 (5180/20 MHz)'), - (CHANNEL_5G_38, '38 (5190/40 MHz)'), - (CHANNEL_5G_40, '40 (5200/20 MHz)'), - (CHANNEL_5G_42, '42 (5210/80 MHz)'), - (CHANNEL_5G_44, '44 (5220/20 MHz)'), - (CHANNEL_5G_46, '46 (5230/40 MHz)'), - (CHANNEL_5G_48, '48 (5240/20 MHz)'), - (CHANNEL_5G_50, '50 (5250/160 MHz)'), - (CHANNEL_5G_52, '52 (5260/20 MHz)'), - (CHANNEL_5G_54, '54 (5270/40 MHz)'), - (CHANNEL_5G_56, '56 (5280/20 MHz)'), - (CHANNEL_5G_58, '58 (5290/80 MHz)'), - (CHANNEL_5G_60, '60 (5300/20 MHz)'), - (CHANNEL_5G_62, '62 (5310/40 MHz)'), - (CHANNEL_5G_64, '64 (5320/20 MHz)'), - (CHANNEL_5G_100, '100 (5500/20 MHz)'), - (CHANNEL_5G_102, '102 (5510/40 MHz)'), - (CHANNEL_5G_104, '104 (5520/20 MHz)'), - (CHANNEL_5G_106, '106 (5530/80 MHz)'), - (CHANNEL_5G_108, '108 (5540/20 MHz)'), - (CHANNEL_5G_110, '110 (5550/40 MHz)'), - (CHANNEL_5G_112, '112 (5560/20 MHz)'), - (CHANNEL_5G_114, '114 (5570/160 MHz)'), - (CHANNEL_5G_116, '116 (5580/20 MHz)'), - (CHANNEL_5G_118, '118 (5590/40 MHz)'), - (CHANNEL_5G_120, '120 (5600/20 MHz)'), - (CHANNEL_5G_122, '122 (5610/80 MHz)'), - (CHANNEL_5G_124, '124 (5620/20 MHz)'), - (CHANNEL_5G_126, '126 (5630/40 MHz)'), - (CHANNEL_5G_128, '128 (5640/20 MHz)'), - (CHANNEL_5G_132, '132 (5660/20 MHz)'), - (CHANNEL_5G_134, '134 (5670/40 MHz)'), - (CHANNEL_5G_136, '136 (5680/20 MHz)'), - (CHANNEL_5G_138, '138 (5690/80 MHz)'), - (CHANNEL_5G_140, '140 (5700/20 MHz)'), - (CHANNEL_5G_142, '142 (5710/40 MHz)'), - (CHANNEL_5G_144, '144 (5720/20 MHz)'), - (CHANNEL_5G_149, '149 (5745/20 MHz)'), - (CHANNEL_5G_151, '151 (5755/40 MHz)'), - (CHANNEL_5G_153, '153 (5765/20 MHz)'), - (CHANNEL_5G_155, '155 (5775/80 MHz)'), - (CHANNEL_5G_157, '157 (5785/20 MHz)'), - (CHANNEL_5G_159, '159 (5795/40 MHz)'), - (CHANNEL_5G_161, '161 (5805/20 MHz)'), - (CHANNEL_5G_163, '163 (5815/160 MHz)'), - (CHANNEL_5G_165, '165 (5825/20 MHz)'), - (CHANNEL_5G_167, '167 (5835/40 MHz)'), - (CHANNEL_5G_169, '169 (5845/20 MHz)'), - (CHANNEL_5G_171, '171 (5855/80 MHz)'), - (CHANNEL_5G_173, '173 (5865/20 MHz)'), - (CHANNEL_5G_175, '175 (5875/40 MHz)'), - (CHANNEL_5G_177, '177 (5885/20 MHz)'), - ) - ), - ) - - -class WirelessChannelWidthChoices(ChoiceSet): - - CHANNEL_WIDTH_20 = 20 - CHANNEL_WIDTH_40 = 40 - CHANNEL_WIDTH_80 = 80 - CHANNEL_WIDTH_160 = 160 - - CHOICES = ( - (CHANNEL_WIDTH_20, '20 MHz'), - (CHANNEL_WIDTH_40, '40 MHz'), - (CHANNEL_WIDTH_80, '80 MHz'), - (CHANNEL_WIDTH_160, '160 MHz'), - ) - - # # PowerFeeds # diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 4deda9df697..5ca009dee1e 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -11,6 +11,7 @@ from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField from virtualization.models import Cluster +from wireless.choices import WirelessRoleChoices __all__ = ( 'CableCSVForm', diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 6334cbff6d6..50954e53423 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -12,6 +12,7 @@ APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) +from wireless.choices import * __all__ = ( 'CableFilterForm', diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 3998dcbc1ed..ff8a19d473c 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -10,6 +10,7 @@ add_blank_choice, BootstrapMixin, ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, StaticSelect, ) +from wireless.choices import * from .common import InterfaceCommonForm __all__ = ( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 39c618f4d53..381a2dcf63f 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -18,6 +18,7 @@ from utilities.ordering import naturalize_interface from utilities.querysets import RestrictedQuerySet from utilities.query_functions import CollateAsChar +from wireless.choices import * __all__ = ( diff --git a/netbox/wireless/choices.py b/netbox/wireless/choices.py new file mode 100644 index 00000000000..f33bf75a140 --- /dev/null +++ b/netbox/wireless/choices.py @@ -0,0 +1,182 @@ +from utilities.choices import ChoiceSet + + +class WirelessRoleChoices(ChoiceSet): + ROLE_AP = 'ap' + ROLE_STATION = 'station' + + CHOICES = ( + (ROLE_AP, 'Access point'), + (ROLE_STATION, 'Station'), + ) + + +class WirelessChannelChoices(ChoiceSet): + CHANNEL_AUTO = 'auto' + + # 2.4 GHz + CHANNEL_24G_1 = '2.4g-1' + CHANNEL_24G_2 = '2.4g-2' + CHANNEL_24G_3 = '2.4g-3' + CHANNEL_24G_4 = '2.4g-4' + CHANNEL_24G_5 = '2.4g-5' + CHANNEL_24G_6 = '2.4g-6' + CHANNEL_24G_7 = '2.4g-7' + CHANNEL_24G_8 = '2.4g-8' + CHANNEL_24G_9 = '2.4g-9' + CHANNEL_24G_10 = '2.4g-10' + CHANNEL_24G_11 = '2.4g-11' + CHANNEL_24G_12 = '2.4g-12' + CHANNEL_24G_13 = '2.4g-13' + + # 5 GHz + CHANNEL_5G_32 = '5g-32' + CHANNEL_5G_34 = '5g-34' + CHANNEL_5G_36 = '5g-36' + CHANNEL_5G_38 = '5g-38' + CHANNEL_5G_40 = '5g-40' + CHANNEL_5G_42 = '5g-42' + CHANNEL_5G_44 = '5g-44' + CHANNEL_5G_46 = '5g-46' + CHANNEL_5G_48 = '5g-48' + CHANNEL_5G_50 = '5g-50' + CHANNEL_5G_52 = '5g-52' + CHANNEL_5G_54 = '5g-54' + CHANNEL_5G_56 = '5g-56' + CHANNEL_5G_58 = '5g-58' + CHANNEL_5G_60 = '5g-60' + CHANNEL_5G_62 = '5g-62' + CHANNEL_5G_64 = '5g-64' + CHANNEL_5G_100 = '5g-100' + CHANNEL_5G_102 = '5g-102' + CHANNEL_5G_104 = '5g-104' + CHANNEL_5G_106 = '5g-106' + CHANNEL_5G_108 = '5g-108' + CHANNEL_5G_110 = '5g-110' + CHANNEL_5G_112 = '5g-112' + CHANNEL_5G_114 = '5g-114' + CHANNEL_5G_116 = '5g-116' + CHANNEL_5G_118 = '5g-118' + CHANNEL_5G_120 = '5g-120' + CHANNEL_5G_122 = '5g-122' + CHANNEL_5G_124 = '5g-124' + CHANNEL_5G_126 = '5g-126' + CHANNEL_5G_128 = '5g-128' + CHANNEL_5G_132 = '5g-132' + CHANNEL_5G_134 = '5g-134' + CHANNEL_5G_136 = '5g-136' + CHANNEL_5G_138 = '5g-138' + CHANNEL_5G_140 = '5g-140' + CHANNEL_5G_142 = '5g-142' + CHANNEL_5G_144 = '5g-144' + CHANNEL_5G_149 = '5g-149' + CHANNEL_5G_151 = '5g-151' + CHANNEL_5G_153 = '5g-153' + CHANNEL_5G_155 = '5g-155' + CHANNEL_5G_157 = '5g-157' + CHANNEL_5G_159 = '5g-159' + CHANNEL_5G_161 = '5g-161' + CHANNEL_5G_163 = '5g-163' + CHANNEL_5G_165 = '5g-165' + CHANNEL_5G_167 = '5g-167' + CHANNEL_5G_169 = '5g-169' + CHANNEL_5G_171 = '5g-171' + CHANNEL_5G_173 = '5g-173' + CHANNEL_5G_175 = '5g-175' + CHANNEL_5G_177 = '5g-177' + + CHOICES = ( + (CHANNEL_AUTO, 'Auto'), + ( + '2.4 GHz (802.11b/g/n/ax)', + ( + (CHANNEL_24G_1, '1 (2412 MHz)'), + (CHANNEL_24G_2, '2 (2417 MHz)'), + (CHANNEL_24G_3, '3 (2422 MHz)'), + (CHANNEL_24G_4, '4 (2427 MHz)'), + (CHANNEL_24G_5, '5 (2432 MHz)'), + (CHANNEL_24G_6, '6 (2437 MHz)'), + (CHANNEL_24G_7, '7 (2442 MHz)'), + (CHANNEL_24G_8, '8 (2447 MHz)'), + (CHANNEL_24G_9, '9 (2452 MHz)'), + (CHANNEL_24G_10, '10 (2457 MHz)'), + (CHANNEL_24G_11, '11 (2462 MHz)'), + (CHANNEL_24G_12, '12 (2467 MHz)'), + (CHANNEL_24G_13, '13 (2472 MHz)'), + ) + ), + ( + '5 GHz (802.11a/n/ac/ax)', + ( + (CHANNEL_5G_32, '32 (5160/20 MHz)'), + (CHANNEL_5G_34, '34 (5170/40 MHz)'), + (CHANNEL_5G_36, '36 (5180/20 MHz)'), + (CHANNEL_5G_38, '38 (5190/40 MHz)'), + (CHANNEL_5G_40, '40 (5200/20 MHz)'), + (CHANNEL_5G_42, '42 (5210/80 MHz)'), + (CHANNEL_5G_44, '44 (5220/20 MHz)'), + (CHANNEL_5G_46, '46 (5230/40 MHz)'), + (CHANNEL_5G_48, '48 (5240/20 MHz)'), + (CHANNEL_5G_50, '50 (5250/160 MHz)'), + (CHANNEL_5G_52, '52 (5260/20 MHz)'), + (CHANNEL_5G_54, '54 (5270/40 MHz)'), + (CHANNEL_5G_56, '56 (5280/20 MHz)'), + (CHANNEL_5G_58, '58 (5290/80 MHz)'), + (CHANNEL_5G_60, '60 (5300/20 MHz)'), + (CHANNEL_5G_62, '62 (5310/40 MHz)'), + (CHANNEL_5G_64, '64 (5320/20 MHz)'), + (CHANNEL_5G_100, '100 (5500/20 MHz)'), + (CHANNEL_5G_102, '102 (5510/40 MHz)'), + (CHANNEL_5G_104, '104 (5520/20 MHz)'), + (CHANNEL_5G_106, '106 (5530/80 MHz)'), + (CHANNEL_5G_108, '108 (5540/20 MHz)'), + (CHANNEL_5G_110, '110 (5550/40 MHz)'), + (CHANNEL_5G_112, '112 (5560/20 MHz)'), + (CHANNEL_5G_114, '114 (5570/160 MHz)'), + (CHANNEL_5G_116, '116 (5580/20 MHz)'), + (CHANNEL_5G_118, '118 (5590/40 MHz)'), + (CHANNEL_5G_120, '120 (5600/20 MHz)'), + (CHANNEL_5G_122, '122 (5610/80 MHz)'), + (CHANNEL_5G_124, '124 (5620/20 MHz)'), + (CHANNEL_5G_126, '126 (5630/40 MHz)'), + (CHANNEL_5G_128, '128 (5640/20 MHz)'), + (CHANNEL_5G_132, '132 (5660/20 MHz)'), + (CHANNEL_5G_134, '134 (5670/40 MHz)'), + (CHANNEL_5G_136, '136 (5680/20 MHz)'), + (CHANNEL_5G_138, '138 (5690/80 MHz)'), + (CHANNEL_5G_140, '140 (5700/20 MHz)'), + (CHANNEL_5G_142, '142 (5710/40 MHz)'), + (CHANNEL_5G_144, '144 (5720/20 MHz)'), + (CHANNEL_5G_149, '149 (5745/20 MHz)'), + (CHANNEL_5G_151, '151 (5755/40 MHz)'), + (CHANNEL_5G_153, '153 (5765/20 MHz)'), + (CHANNEL_5G_155, '155 (5775/80 MHz)'), + (CHANNEL_5G_157, '157 (5785/20 MHz)'), + (CHANNEL_5G_159, '159 (5795/40 MHz)'), + (CHANNEL_5G_161, '161 (5805/20 MHz)'), + (CHANNEL_5G_163, '163 (5815/160 MHz)'), + (CHANNEL_5G_165, '165 (5825/20 MHz)'), + (CHANNEL_5G_167, '167 (5835/40 MHz)'), + (CHANNEL_5G_169, '169 (5845/20 MHz)'), + (CHANNEL_5G_171, '171 (5855/80 MHz)'), + (CHANNEL_5G_173, '173 (5865/20 MHz)'), + (CHANNEL_5G_175, '175 (5875/40 MHz)'), + (CHANNEL_5G_177, '177 (5885/20 MHz)'), + ) + ), + ) + + +class WirelessChannelWidthChoices(ChoiceSet): + + CHANNEL_WIDTH_20 = 20 + CHANNEL_WIDTH_40 = 40 + CHANNEL_WIDTH_80 = 80 + CHANNEL_WIDTH_160 = 160 + + CHOICES = ( + (CHANNEL_WIDTH_20, '20 MHz'), + (CHANNEL_WIDTH_40, '40 MHz'), + (CHANNEL_WIDTH_80, '80 MHz'), + (CHANNEL_WIDTH_160, '160 MHz'), + ) From b7317bfe2911f792ead028db80d5797ba18ac166 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Oct 2021 10:06:49 -0400 Subject: [PATCH 21/28] Remove choices from rf_channel_width --- netbox/dcim/api/serializers.py | 1 - netbox/dcim/forms/filtersets.py | 6 ++---- netbox/dcim/forms/models.py | 1 - netbox/dcim/forms/object_create.py | 4 +--- netbox/dcim/models/device_components.py | 3 +-- netbox/templates/dcim/interface.html | 2 +- netbox/wireless/choices.py | 15 --------------- 7 files changed, 5 insertions(+), 27 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d7857d5e8c9..d2a9125c0fd 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -637,7 +637,6 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True) rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False) - rf_channel_width = ChoiceField(choices=WirelessChannelWidthChoices, required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 50954e53423..fb9449d419f 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1014,11 +1014,9 @@ class InterfaceFilterForm(DeviceComponentFilterForm): widget=StaticSelectMultiple(), label='Wireless channel' ) - rf_channel_width = forms.MultipleChoiceField( - choices=WirelessChannelWidthChoices, + rf_channel_width = forms.IntegerField( required=False, - widget=StaticSelectMultiple(), - label='Channel width' + label='Channel width (kHz)' ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 35b90291e02..675319f1185 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1117,7 +1117,6 @@ class Meta: 'mode': StaticSelect(), 'rf_role': StaticSelect(), 'rf_channel': StaticSelect(), - 'rf_channel_width': StaticSelect(), } labels = { 'mode': '802.1Q Mode', diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index ff8a19d473c..f924fb5d961 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -480,10 +480,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): widget=StaticSelect(), label='Wireless channel' ) - rf_channel_width = forms.ChoiceField( - choices=add_blank_choice(WirelessChannelWidthChoices), + rf_channel_width = forms.IntegerField( required=False, - widget=StaticSelect(), label='Channel width' ) untagged_vlan = DynamicModelChoiceField( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 381a2dcf63f..bb18e0c26b8 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -538,10 +538,9 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint): verbose_name='Wireless channel' ) rf_channel_width = models.PositiveSmallIntegerField( - choices=WirelessChannelWidthChoices, blank=True, null=True, - verbose_name='Channel width' + verbose_name='Channel width (kHz)' ) wireless_link = models.ForeignKey( to='wireless.WirelessLink', diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 90c9497ef96..427ea835211 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -278,7 +278,7 @@
Wireless
Channel Width - {{ object.get_rf_channel_width_display|placeholder }} + {{ object.rf_channel_width|placeholder }}
diff --git a/netbox/wireless/choices.py b/netbox/wireless/choices.py index f33bf75a140..1369ee340ac 100644 --- a/netbox/wireless/choices.py +++ b/netbox/wireless/choices.py @@ -165,18 +165,3 @@ class WirelessChannelChoices(ChoiceSet): ) ), ) - - -class WirelessChannelWidthChoices(ChoiceSet): - - CHANNEL_WIDTH_20 = 20 - CHANNEL_WIDTH_40 = 40 - CHANNEL_WIDTH_80 = 80 - CHANNEL_WIDTH_160 = 160 - - CHOICES = ( - (CHANNEL_WIDTH_20, '20 MHz'), - (CHANNEL_WIDTH_40, '40 MHz'), - (CHANNEL_WIDTH_80, '80 MHz'), - (CHANNEL_WIDTH_160, '160 MHz'), - ) From 075f4907ef7e3da89c1e3f8aed418432857da9d3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Oct 2021 11:35:10 -0400 Subject: [PATCH 22/28] Store channel frequency & width as independent values --- netbox/dcim/api/serializers.py | 8 +- netbox/dcim/forms/bulk_edit.py | 4 +- netbox/dcim/forms/bulk_import.py | 3 +- netbox/dcim/forms/filtersets.py | 6 +- netbox/dcim/forms/models.py | 6 +- netbox/dcim/forms/object_create.py | 12 +- netbox/dcim/migrations/0138_wireless.py | 7 +- netbox/dcim/models/device_components.py | 40 +++++- netbox/dcim/tables/devices.py | 5 +- netbox/templates/dcim/interface.html | 18 ++- netbox/templates/dcim/interface_edit.html | 1 + .../wireless/inc/wirelesslink_interface.html | 20 +++ netbox/utilities/templatetags/helpers.py | 14 ++ netbox/wireless/choices.py | 136 +++++++++--------- netbox/wireless/forms/models.py | 10 +- netbox/wireless/utils.py | 27 ++++ 16 files changed, 223 insertions(+), 94 deletions(-) create mode 100644 netbox/wireless/utils.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d2a9125c0fd..4eeb717d73d 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -651,10 +651,10 @@ class Meta: model = Interface fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', 'untagged_vlan', - 'tagged_vlans', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', - 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', 'count_ipaddresses', '_occupied', + 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', + 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer', + 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', + 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied', ] def validate(self, data): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 9c6a8588562..e8e60a4f9a0 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -936,7 +936,7 @@ def __init__(self, *args, **kwargs): class InterfaceBulkEditForm( form_from_model(Interface, [ 'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', - 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', + 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', ]), BootstrapMixin, AddRemoveTagsForm, @@ -988,7 +988,7 @@ class InterfaceBulkEditForm( class Meta: nullable_fields = [ 'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel', - 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', + 'rf_channel_frequency', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 5ca009dee1e..4eb8608360a 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -595,7 +595,8 @@ class Meta: model = Interface fields = ( 'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn', - 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', + 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', + 'rf_channel_width', ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index fb9449d419f..e28714914d8 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1014,9 +1014,13 @@ class InterfaceFilterForm(DeviceComponentFilterForm): widget=StaticSelectMultiple(), label='Wireless channel' ) + rf_channel_frequency = forms.IntegerField( + required=False, + label='Channel frequency (MHz)' + ) rf_channel_width = forms.IntegerField( required=False, - label='Channel width (kHz)' + label='Channel width (MHz)' ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 675319f1185..6037675182f 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1108,8 +1108,8 @@ class Meta: model = Interface fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', - 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_width', 'wireless_lans', - 'untagged_vlan', 'tagged_vlans', 'tags', + 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', + 'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -1123,6 +1123,8 @@ class Meta: } help_texts = { 'mode': INTERFACE_MODE_HELP_TEXT, + 'rf_channel_frequency': "Populated by selected channel (if set)", + 'rf_channel_width': "Populated by selected channel (if set)", } def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index f924fb5d961..547fe7e68f5 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -480,9 +480,13 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): widget=StaticSelect(), label='Wireless channel' ) - rf_channel_width = forms.IntegerField( + rf_channel_frequency = forms.DecimalField( required=False, - label='Channel width' + label='Channel frequency (MHz)' + ) + rf_channel_width = forms.DecimalField( + required=False, + label='Channel width (MHz)' ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), @@ -494,8 +498,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): ) field_order = ( 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', - 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_width', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags' + 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency', + 'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags' ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/migrations/0138_wireless.py b/netbox/dcim/migrations/0138_wireless.py index faebcf2680f..bbdb282835c 100644 --- a/netbox/dcim/migrations/0138_wireless.py +++ b/netbox/dcim/migrations/0138_wireless.py @@ -20,10 +20,15 @@ class Migration(migrations.Migration): name='rf_channel', field=models.CharField(blank=True, max_length=50), ), + migrations.AddField( + model_name='interface', + name='rf_channel_frequency', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True), + ), migrations.AddField( model_name='interface', name='rf_channel_width', - field=models.PositiveSmallIntegerField(blank=True, null=True), + field=models.DecimalField(blank=True, decimal_places=3, max_digits=7, null=True), ), migrations.AddField( model_name='interface', diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index bb18e0c26b8..c2a37fcae4f 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -19,6 +19,7 @@ from utilities.querysets import RestrictedQuerySet from utilities.query_functions import CollateAsChar from wireless.choices import * +from wireless.utils import get_channel_attr __all__ = ( @@ -537,10 +538,19 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint): blank=True, verbose_name='Wireless channel' ) - rf_channel_width = models.PositiveSmallIntegerField( + rf_channel_frequency = models.DecimalField( + max_digits=7, + decimal_places=2, blank=True, null=True, - verbose_name='Channel width (kHz)' + verbose_name='Channel frequency (MHz)' + ) + rf_channel_width = models.DecimalField( + max_digits=7, + decimal_places=3, + blank=True, + null=True, + verbose_name='Channel width (MHz)' ) wireless_link = models.ForeignKey( to='wireless.WirelessLink', @@ -641,13 +651,33 @@ def clean(self): if self.pk and self.lag_id == self.pk: raise ValidationError({'lag': "A LAG interface cannot be its own parent."}) - # RF channel attributes may be set only for wireless interfaces + # RF role & channel may only be set for wireless interfaces if self.rf_role and not self.is_wireless: raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."}) if self.rf_channel and not self.is_wireless: raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."}) - if self.rf_channel_width and not self.is_wireless: - raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."}) + + # Validate channel frequency against interface type and selected channel (if any) + if self.rf_channel_frequency: + if not self.is_wireless: + raise ValidationError({ + 'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.", + }) + if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'): + raise ValidationError({ + 'rf_channel_frequency': "Cannot specify custom frequency with channel selected.", + }) + elif self.rf_channel: + self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency') + + # Validate channel width against interface type and selected channel (if any) + if self.rf_channel_width: + if not self.is_wireless: + raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."}) + if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'): + raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."}) + elif self.rf_channel: + self.rf_channel_width = get_channel_attr(self.rf_channel, 'width') # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]: diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index b0ef9807ece..3b0ec349ef9 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -496,8 +496,9 @@ class Meta(DeviceComponentTable.Meta): model = Interface fields = ( 'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', - 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', + 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'description', 'mark_connected', + 'cable', 'cable_color', 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', + 'tagged_vlans', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 427ea835211..6e01dee98e8 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -276,9 +276,25 @@
Wireless
Channel {{ object.get_rf_channel_display|placeholder }} + + Channel Frequency + + {% if object.rf_channel_frequency %} + {{ object.rf_channel_frequency|simplify_decimal }} MHz + {% else %} + + {% endif %} + + Channel Width - {{ object.rf_channel_width|placeholder }} + + {% if object.rf_channel_width %} + {{ object.rf_channel_width|simplify_decimal }} MHz + {% else %} + + {% endif %} + diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index cb8d5182818..de7d21269e9 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -36,6 +36,7 @@
Wireless
{% render_field form.rf_role %} {% render_field form.rf_channel %} + {% render_field form.rf_channel_frequency %} {% render_field form.rf_channel_width %} {% render_field form.wireless_lans %} diff --git a/netbox/templates/wireless/inc/wirelesslink_interface.html b/netbox/templates/wireless/inc/wirelesslink_interface.html index 82f7cfd8d0d..e330475396f 100644 --- a/netbox/templates/wireless/inc/wirelesslink_interface.html +++ b/netbox/templates/wireless/inc/wirelesslink_interface.html @@ -31,4 +31,24 @@ {{ interface.get_rf_channel_display|placeholder }} + + Channel Frequency + + {% if interface.rf_channel_frequency %} + {{ interface.rf_channel_frequency|simplify_decimal }} MHz + {% else %} + + {% endif %} + + + + Channel Width + + {% if interface.rf_channel_width %} + {{ interface.rf_channel_width|simplify_decimal }} MHz + {% else %} + + {% endif %} + + diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index a900d59e2b9..668596c8e88 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,4 +1,5 @@ import datetime +import decimal import json import re from typing import Dict, Any @@ -146,6 +147,19 @@ def humanize_megabytes(mb): return f'{mb} MB' +@register.filter() +def simplify_decimal(value): + """ + Return the simplest expression of a decimal value. Examples: + 1.00 => '1' + 1.20 => '1.2' + 1.23 => '1.23' + """ + if type(value) is not decimal.Decimal: + return value + return str(value).rstrip('0.') + + @register.filter() def tzoffset(value): """ diff --git a/netbox/wireless/choices.py b/netbox/wireless/choices.py index 1369ee340ac..8a710b5328d 100644 --- a/netbox/wireless/choices.py +++ b/netbox/wireless/choices.py @@ -12,81 +12,79 @@ class WirelessRoleChoices(ChoiceSet): class WirelessChannelChoices(ChoiceSet): - CHANNEL_AUTO = 'auto' # 2.4 GHz - CHANNEL_24G_1 = '2.4g-1' - CHANNEL_24G_2 = '2.4g-2' - CHANNEL_24G_3 = '2.4g-3' - CHANNEL_24G_4 = '2.4g-4' - CHANNEL_24G_5 = '2.4g-5' - CHANNEL_24G_6 = '2.4g-6' - CHANNEL_24G_7 = '2.4g-7' - CHANNEL_24G_8 = '2.4g-8' - CHANNEL_24G_9 = '2.4g-9' - CHANNEL_24G_10 = '2.4g-10' - CHANNEL_24G_11 = '2.4g-11' - CHANNEL_24G_12 = '2.4g-12' - CHANNEL_24G_13 = '2.4g-13' + CHANNEL_24G_1 = '2.4g-1-2412-22' + CHANNEL_24G_2 = '2.4g-2-2417-22' + CHANNEL_24G_3 = '2.4g-3-2422-22' + CHANNEL_24G_4 = '2.4g-4-2427-22' + CHANNEL_24G_5 = '2.4g-5-2432-22' + CHANNEL_24G_6 = '2.4g-6-2437-22' + CHANNEL_24G_7 = '2.4g-7-2442-22' + CHANNEL_24G_8 = '2.4g-8-2447-22' + CHANNEL_24G_9 = '2.4g-9-2452-22' + CHANNEL_24G_10 = '2.4g-10-2457-22' + CHANNEL_24G_11 = '2.4g-11-2462-22' + CHANNEL_24G_12 = '2.4g-12-2467-22' + CHANNEL_24G_13 = '2.4g-13-2472-22' # 5 GHz - CHANNEL_5G_32 = '5g-32' - CHANNEL_5G_34 = '5g-34' - CHANNEL_5G_36 = '5g-36' - CHANNEL_5G_38 = '5g-38' - CHANNEL_5G_40 = '5g-40' - CHANNEL_5G_42 = '5g-42' - CHANNEL_5G_44 = '5g-44' - CHANNEL_5G_46 = '5g-46' - CHANNEL_5G_48 = '5g-48' - CHANNEL_5G_50 = '5g-50' - CHANNEL_5G_52 = '5g-52' - CHANNEL_5G_54 = '5g-54' - CHANNEL_5G_56 = '5g-56' - CHANNEL_5G_58 = '5g-58' - CHANNEL_5G_60 = '5g-60' - CHANNEL_5G_62 = '5g-62' - CHANNEL_5G_64 = '5g-64' - CHANNEL_5G_100 = '5g-100' - CHANNEL_5G_102 = '5g-102' - CHANNEL_5G_104 = '5g-104' - CHANNEL_5G_106 = '5g-106' - CHANNEL_5G_108 = '5g-108' - CHANNEL_5G_110 = '5g-110' - CHANNEL_5G_112 = '5g-112' - CHANNEL_5G_114 = '5g-114' - CHANNEL_5G_116 = '5g-116' - CHANNEL_5G_118 = '5g-118' - CHANNEL_5G_120 = '5g-120' - CHANNEL_5G_122 = '5g-122' - CHANNEL_5G_124 = '5g-124' - CHANNEL_5G_126 = '5g-126' - CHANNEL_5G_128 = '5g-128' - CHANNEL_5G_132 = '5g-132' - CHANNEL_5G_134 = '5g-134' - CHANNEL_5G_136 = '5g-136' - CHANNEL_5G_138 = '5g-138' - CHANNEL_5G_140 = '5g-140' - CHANNEL_5G_142 = '5g-142' - CHANNEL_5G_144 = '5g-144' - CHANNEL_5G_149 = '5g-149' - CHANNEL_5G_151 = '5g-151' - CHANNEL_5G_153 = '5g-153' - CHANNEL_5G_155 = '5g-155' - CHANNEL_5G_157 = '5g-157' - CHANNEL_5G_159 = '5g-159' - CHANNEL_5G_161 = '5g-161' - CHANNEL_5G_163 = '5g-163' - CHANNEL_5G_165 = '5g-165' - CHANNEL_5G_167 = '5g-167' - CHANNEL_5G_169 = '5g-169' - CHANNEL_5G_171 = '5g-171' - CHANNEL_5G_173 = '5g-173' - CHANNEL_5G_175 = '5g-175' - CHANNEL_5G_177 = '5g-177' + CHANNEL_5G_32 = '5g-32-5160-20' + CHANNEL_5G_34 = '5g-34-5170-40' + CHANNEL_5G_36 = '5g-36-5180-20' + CHANNEL_5G_38 = '5g-38-5190-40' + CHANNEL_5G_40 = '5g-40-5200-20' + CHANNEL_5G_42 = '5g-42-5210-80' + CHANNEL_5G_44 = '5g-44-5220-20' + CHANNEL_5G_46 = '5g-46-5230-40' + CHANNEL_5G_48 = '5g-48-5240-20' + CHANNEL_5G_50 = '5g-50-5250-160' + CHANNEL_5G_52 = '5g-52-5260-20' + CHANNEL_5G_54 = '5g-54-5270-40' + CHANNEL_5G_56 = '5g-56-5280-20' + CHANNEL_5G_58 = '5g-58-5290-80' + CHANNEL_5G_60 = '5g-60-5300-20' + CHANNEL_5G_62 = '5g-62-5310-40' + CHANNEL_5G_64 = '5g-64-5320-20' + CHANNEL_5G_100 = '5g-100-5500-20' + CHANNEL_5G_102 = '5g-102-5510-40' + CHANNEL_5G_104 = '5g-104-5520-20' + CHANNEL_5G_106 = '5g-106-5530-80' + CHANNEL_5G_108 = '5g-108-5540-20' + CHANNEL_5G_110 = '5g-110-5550-40' + CHANNEL_5G_112 = '5g-112-5560-20' + CHANNEL_5G_114 = '5g-114-5570-160' + CHANNEL_5G_116 = '5g-116-5580-20' + CHANNEL_5G_118 = '5g-118-5590-40' + CHANNEL_5G_120 = '5g-120-5600-20' + CHANNEL_5G_122 = '5g-122-5610-80' + CHANNEL_5G_124 = '5g-124-5620-20' + CHANNEL_5G_126 = '5g-126-5630-40' + CHANNEL_5G_128 = '5g-128-5640-20' + CHANNEL_5G_132 = '5g-132-5660-20' + CHANNEL_5G_134 = '5g-134-5670-40' + CHANNEL_5G_136 = '5g-136-5680-20' + CHANNEL_5G_138 = '5g-138-5690-80' + CHANNEL_5G_140 = '5g-140-5700-20' + CHANNEL_5G_142 = '5g-142-5710-40' + CHANNEL_5G_144 = '5g-144-5720-20' + CHANNEL_5G_149 = '5g-149-5745-20' + CHANNEL_5G_151 = '5g-151-5755-40' + CHANNEL_5G_153 = '5g-153-5765-20' + CHANNEL_5G_155 = '5g-155-5775-80' + CHANNEL_5G_157 = '5g-157-5785-20' + CHANNEL_5G_159 = '5g-159-5795-40' + CHANNEL_5G_161 = '5g-161-5805-20' + CHANNEL_5G_163 = '5g-163-5815-160' + CHANNEL_5G_165 = '5g-165-5825-20' + CHANNEL_5G_167 = '5g-167-5835-40' + CHANNEL_5G_169 = '5g-169-5845-20' + CHANNEL_5G_171 = '5g-171-5855-80' + CHANNEL_5G_173 = '5g-173-5865-20' + CHANNEL_5G_175 = '5g-175-5875-40' + CHANNEL_5G_177 = '5g-177-5885-20' CHOICES = ( - (CHANNEL_AUTO, 'Auto'), ( '2.4 GHz (802.11b/g/n/ax)', ( diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index a3454c79a39..9a7b78b31a5 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -56,7 +56,10 @@ class Meta: class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): device_a = DynamicModelChoiceField( queryset=Device.objects.all(), - label='Device A' + label='Device A', + initial_params={ + 'interfaces': '$interface_a' + } ) interface_a = DynamicModelChoiceField( queryset=Interface.objects.all(), @@ -69,7 +72,10 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): ) device_b = DynamicModelChoiceField( queryset=Device.objects.all(), - label='Device B' + label='Device B', + initial_params={ + 'interfaces': '$interface_b' + } ) interface_b = DynamicModelChoiceField( queryset=Interface.objects.all(), diff --git a/netbox/wireless/utils.py b/netbox/wireless/utils.py new file mode 100644 index 00000000000..d98d6a853a0 --- /dev/null +++ b/netbox/wireless/utils.py @@ -0,0 +1,27 @@ +from decimal import Decimal + +from .choices import WirelessChannelChoices + +__all__ = ( + 'get_channel_attr', +) + + +def get_channel_attr(channel, attr): + """ + Return the specified attribute of a given WirelessChannelChoices value. + """ + if channel not in WirelessChannelChoices.values(): + raise ValueError(f"Invalid channel value: {channel}") + + channel_values = channel.split('-') + attrs = { + 'band': channel_values[0], + 'id': int(channel_values[1]), + 'frequency': Decimal(channel_values[2]), + 'width': Decimal(channel_values[3]), + } + if attr not in attrs: + raise ValueError(f"Invalid channel attribute: {attr}") + + return attrs[attr] From 717fd760df62dd9ad1d994c8fc1893169109988a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Oct 2021 12:24:07 -0400 Subject: [PATCH 23/28] #3979: UI cleanup --- netbox/dcim/forms/models.py | 12 +- netbox/dcim/tables/devices.py | 4 +- netbox/dcim/tables/template_code.py | 10 ++ netbox/templates/dcim/interface.html | 128 ++++++++++++++++------ netbox/templates/dcim/interface_edit.html | 1 + netbox/utilities/templatetags/helpers.py | 2 +- netbox/wireless/filtersets.py | 3 + 7 files changed, 124 insertions(+), 36 deletions(-) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 6037675182f..9ce0b54aa37 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -16,7 +16,7 @@ SlugField, StaticSelect, ) from virtualization.models import Cluster, ClusterGroup -from wireless.models import WirelessLAN +from wireless.models import WirelessLAN, WirelessLANGroup from .common import InterfaceCommonForm __all__ = ( @@ -1073,10 +1073,18 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): 'type': 'lag', } ) + wireless_lan_group = DynamicModelChoiceField( + queryset=WirelessLANGroup.objects.all(), + required=False, + label='Wireless LAN group' + ) wireless_lans = DynamicModelMultipleChoiceField( queryset=WirelessLAN.objects.all(), required=False, - label='Wireless LANs' + label='Wireless LANs', + query_params={ + 'group_id': '$wireless_lan_group', + } ) vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 3b0ec349ef9..3b92efd7615 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -505,8 +505,8 @@ class Meta(DeviceComponentTable.Meta): class DeviceInterfaceTable(InterfaceTable): name = tables.TemplateColumn( - template_code=' {{ value }}', order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index a5a4d9979ef..a948baffd87 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -205,6 +205,12 @@ {% endif %} +{% elif record.wireless_link %} + {% if perms.wireless.delete_wirelesslink %} + + + + {% endif %} {% elif record.is_wired and perms.dcim.add_cable %} @@ -223,6 +229,10 @@ {% else %} {% endif %} +{% elif record.is_wireless and perms.wireless.add_wirelesslink %} + + + {% endif %} """ diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 6e01dee98e8..e9230fbf999 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -217,8 +217,29 @@
Wireless Link {{ object.wireless_link }} + + + + {% with peer_interface=object.connected_endpoint %} + + Device + + {{ peer_interface.device }} + + + + Name + + {{ peer_interface }} + + + + Type + {{ peer_interface.get_type_display }} + + {% endwith %} {% else %}
@@ -267,36 +288,73 @@
Wireless
- - - - - - - - - - - - - - - - - -
Role{{ object.get_rf_role_display|placeholder }}
Channel{{ object.get_rf_channel_display|placeholder }}
Channel Frequency - {% if object.rf_channel_frequency %} - {{ object.rf_channel_frequency|simplify_decimal }} MHz - {% else %} - - {% endif %} -
Channel Width - {% if object.rf_channel_width %} - {{ object.rf_channel_width|simplify_decimal }} MHz - {% else %} - - {% endif %} -
+ {% with peer=object.connected_endpoint %} + + + + + + {% if peer %} + + {% endif %} + + + + + + {% if peer %} + + {% endif %} + + + + + {% if peer %} + + {{ peer.get_rf_channel_display|placeholder }} + + {% endif %} + + + + + {% if peer %} + + {% if peer.rf_channel_frequency %} + {{ peer.rf_channel_frequency|simplify_decimal }} MHz + {% else %} + + {% endif %} + + {% endif %} + + + + + {% if peer %} + + {% if peer.rf_channel_width %} + {{ peer.rf_channel_width|simplify_decimal }} MHz + {% else %} + + {% endif %} + + {% endif %} + +
LocalPeer
Role{{ object.get_rf_role_display|placeholder }}{{ peer.get_rf_role_display|placeholder }}
Channel{{ object.get_rf_channel_display|placeholder }}
Channel Frequency + {% if object.rf_channel_frequency %} + {{ object.rf_channel_frequency|simplify_decimal }} MHz + {% else %} + + {% endif %} +
Channel Width + {% if object.rf_channel_width %} + {{ object.rf_channel_width|simplify_decimal }} MHz + {% else %} + + {% endif %} +
+ {% endwith %}
@@ -305,12 +363,20 @@
Wireless LANs
+ - {% for wlan in object.wlans.all %} + {% for wlan in object.wireless_lans.all %} + diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index de7d21269e9..aec88d25a07 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -38,6 +38,7 @@
Wireless
{% render_field form.rf_channel %} {% render_field form.rf_channel_frequency %} {% render_field form.rf_channel_width %} + {% render_field form.wireless_lan_group %} {% render_field form.wireless_lans %} {% endif %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 668596c8e88..3318fe1e7a1 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -157,7 +157,7 @@ def simplify_decimal(value): """ if type(value) is not decimal.Decimal: return value - return str(value).rstrip('0.') + return str(value).rstrip('0').rstrip('.') @register.filter() diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index ac503e474d3..a5d9b7d75c5 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -33,6 +33,9 @@ class WirelessLANFilterSet(PrimaryModelFilterSet): method='search', label='Search', ) + group_id = django_filters.ModelMultipleChoiceFilter( + queryset=WirelessLANGroup.objects.all() + ) tag = TagFilter() class Meta: From 0c72c20d2aa1b3db087eb63613fc087528a2095b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Oct 2021 10:05:43 -0400 Subject: [PATCH 24/28] Add WirelessLANs column to interfaces table --- netbox/dcim/tables/devices.py | 19 ++++++++++--------- netbox/dcim/tables/template_code.py | 6 ++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 3b92efd7615..343667d4663 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -11,11 +11,7 @@ BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn, ) -from .template_code import ( - LINKTERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS, - FRONTPORT_BUTTONS, INTERFACE_BUTTONS, INTERFACE_IPADDRESSES, INTERFACE_TAGGED_VLANS, POWEROUTLET_BUTTONS, - POWERPORT_BUTTONS, REARPORT_BUTTONS, -) +from .template_code import * __all__ = ( 'BaseInterfaceTable', @@ -488,6 +484,11 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable wireless_link = tables.Column( linkify=True ) + wireless_lans = TemplateColumn( + template_code=INTERFACE_WIRELESS_LANS, + orderable=False, + verbose_name='Wireless LANs' + ) tags = TagColumn( url_name='dcim:interface_list' ) @@ -497,8 +498,8 @@ class Meta(DeviceComponentTable.Meta): fields = ( 'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'description', 'mark_connected', - 'cable', 'cable_color', 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', - 'tagged_vlans', + 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', + 'untagged_vlan', 'tagged_vlans', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') @@ -530,8 +531,8 @@ class Meta(DeviceComponentTable.Meta): fields = ( 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', - 'wireless_link', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', - 'actions', + 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', + 'tagged_vlans', 'actions', ) order_by = ('name',) default_columns = ( diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index a948baffd87..aab15b5efcf 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -64,6 +64,12 @@ {% endif %} """ +INTERFACE_WIRELESS_LANS = """ +{% for wlan in record.wireless_lans.all %} + {{ wlan }}
+{% endfor %} +""" + POWERFEED_CABLE = """ {{ value }} From a66501250e10f09641e5b0bef927db10014323aa Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 10:58:15 -0400 Subject: [PATCH 25/28] Add wireless authentication attributes --- .../wireless/inc/authentication_attrs.html | 21 ++++++++++ netbox/templates/wireless/wirelesslan.html | 3 +- netbox/templates/wireless/wirelesslink.html | 7 +++- netbox/wireless/api/serializers.py | 10 ++++- netbox/wireless/choices.py | 26 ++++++++++++ netbox/wireless/constants.py | 1 + netbox/wireless/filtersets.py | 17 +++++++- netbox/wireless/forms/bulk_edit.py | 27 +++++++++++- netbox/wireless/forms/bulk_import.py | 25 ++++++++++- netbox/wireless/forms/filtersets.py | 27 ++++++++++++ netbox/wireless/forms/models.py | 19 +++++++-- .../wireless/migrations/0002_wireless_auth.py | 41 +++++++++++++++++++ netbox/wireless/models.py | 34 +++++++++++++-- netbox/wireless/tables.py | 15 +++++-- 14 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 netbox/templates/wireless/inc/authentication_attrs.html create mode 100644 netbox/wireless/migrations/0002_wireless_auth.py diff --git a/netbox/templates/wireless/inc/authentication_attrs.html b/netbox/templates/wireless/inc/authentication_attrs.html new file mode 100644 index 00000000000..ed4c7546cb1 --- /dev/null +++ b/netbox/templates/wireless/inc/authentication_attrs.html @@ -0,0 +1,21 @@ +{% load helpers %} + +
+
Authentication
+
+
Group SSID
+ {% if wlan.group %} + {{ wlan.group }} + {% else %} + — + {% endif %} + {{ wlan.ssid }}
+ + + + + + + + + + + + +
Type{{ object.get_auth_type_display|placeholder }}
Cipher{{ object.get_auth_cipher_display|placeholder }}
PSK{{ object.auth_psk|placeholder }}
+
+
diff --git a/netbox/templates/wireless/wirelesslan.html b/netbox/templates/wireless/wirelesslan.html index cfe13ca45e4..5c6784de4a7 100644 --- a/netbox/templates/wireless/wirelesslan.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -40,10 +40,11 @@
Wireless LAN
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='wireless:wirelesslan_list' %} + {% include 'wireless/inc/authentication_attrs.html' %} {% plugin_left_page object %}
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='wireless:wirelesslan_list' %} {% include 'inc/custom_fields_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/wireless/wirelesslink.html b/netbox/templates/wireless/wirelesslink.html index 45ec6b0c906..afdeff35794 100644 --- a/netbox/templates/wireless/wirelesslink.html +++ b/netbox/templates/wireless/wirelesslink.html @@ -17,7 +17,9 @@
Link Properties
- + @@ -30,6 +32,7 @@
Link Properties
Status{{ object.get_status_display }} + {{ object.get_status_display }} +
SSID
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='wireless:wirelesslink_list' %} {% plugin_left_page object %}
@@ -39,8 +42,8 @@
Interface B
{% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_b %}
+ {% include 'wireless/inc/authentication_attrs.html' %} {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='wireless:wirelesslink_list' %} {% plugin_right_page object %} diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 24395b77c52..e9be3561859 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -5,6 +5,7 @@ from ipam.api.serializers import NestedVLANSerializer from netbox.api import ChoiceField from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer +from wireless.choices import * from wireless.models import * from .nested_serializers import * @@ -30,11 +31,13 @@ class Meta: class WirelessLANSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') vlan = NestedVLANSerializer(required=False, allow_null=True) + auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) + auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) class Meta: model = WirelessLAN fields = [ - 'id', 'url', 'display', 'ssid', 'description', 'vlan', + 'id', 'url', 'display', 'ssid', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', ] @@ -43,9 +46,12 @@ class WirelessLinkSerializer(PrimaryModelSerializer): status = ChoiceField(choices=LinkStatusChoices, required=False) interface_a = NestedInterfaceSerializer() interface_b = NestedInterfaceSerializer() + auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) + auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) class Meta: model = WirelessLink fields = [ - 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', 'auth_type', + 'auth_cipher', 'auth_psk', ] diff --git a/netbox/wireless/choices.py b/netbox/wireless/choices.py index 8a710b5328d..c8e7fd09fda 100644 --- a/netbox/wireless/choices.py +++ b/netbox/wireless/choices.py @@ -163,3 +163,29 @@ class WirelessChannelChoices(ChoiceSet): ) ), ) + + +class WirelessAuthTypeChoices(ChoiceSet): + TYPE_OPEN = 'open' + TYPE_WEP = 'wep' + TYPE_WPA_PERSONAL = 'wpa-personal' + TYPE_WPA_ENTERPRISE = 'wpa-enterprise' + + CHOICES = ( + (TYPE_OPEN, 'Open'), + (TYPE_WEP, 'WEP'), + (TYPE_WPA_PERSONAL, 'WPA Personal (PSK)'), + (TYPE_WPA_ENTERPRISE, 'WPA Enterprise'), + ) + + +class WirelessAuthCipherChoices(ChoiceSet): + CIPHER_AUTO = 'auto' + CIPHER_TKIP = 'tkip' + CIPHER_AES = 'aes' + + CHOICES = ( + (CIPHER_AUTO, 'Auto'), + (CIPHER_TKIP, 'TKIP'), + (CIPHER_AES, 'AES'), + ) diff --git a/netbox/wireless/constants.py b/netbox/wireless/constants.py index 188c4abd935..63de2b13647 100644 --- a/netbox/wireless/constants.py +++ b/netbox/wireless/constants.py @@ -1 +1,2 @@ SSID_MAX_LENGTH = 32 # Per IEEE 802.11-2007 +PSK_MAX_LENGTH = 64 diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index a5d9b7d75c5..cc67c1fc39c 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -4,6 +4,7 @@ from dcim.choices import LinkStatusChoices from extras.filters import TagFilter from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet +from .choices import * from .models import * __all__ = ( @@ -36,11 +37,17 @@ class WirelessLANFilterSet(PrimaryModelFilterSet): group_id = django_filters.ModelMultipleChoiceFilter( queryset=WirelessLANGroup.objects.all() ) + auth_type = django_filters.MultipleChoiceFilter( + choices=WirelessAuthTypeChoices + ) + auth_cipher = django_filters.MultipleChoiceFilter( + choices=WirelessAuthCipherChoices + ) tag = TagFilter() class Meta: model = WirelessLAN - fields = ['id', 'ssid'] + fields = ['id', 'ssid', 'auth_psk'] def search(self, queryset, name, value): if not value.strip(): @@ -60,11 +67,17 @@ class WirelessLinkFilterSet(PrimaryModelFilterSet): status = django_filters.MultipleChoiceFilter( choices=LinkStatusChoices ) + auth_type = django_filters.MultipleChoiceFilter( + choices=WirelessAuthTypeChoices + ) + auth_cipher = django_filters.MultipleChoiceFilter( + choices=WirelessAuthCipherChoices + ) tag = TagFilter() class Meta: model = WirelessLink - fields = ['id', 'ssid'] + fields = ['id', 'ssid', 'auth_psk'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index c0d5a925e3f..1da98026c11 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -4,6 +4,7 @@ from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import VLAN from utilities.forms import BootstrapMixin, DynamicModelChoiceField +from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH from wireless.models import * @@ -52,9 +53,20 @@ class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode description = forms.CharField( required=False ) + auth_type = forms.ChoiceField( + choices=WirelessAuthTypeChoices, + required=False + ) + auth_cipher = forms.ChoiceField( + choices=WirelessAuthCipherChoices, + required=False + ) + auth_psk = forms.CharField( + required=False + ) class Meta: - nullable_fields = ['ssid', 'group', 'vlan', 'description'] + nullable_fields = ['ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk'] class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): @@ -73,6 +85,17 @@ class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMod description = forms.CharField( required=False ) + auth_type = forms.ChoiceField( + choices=WirelessAuthTypeChoices, + required=False + ) + auth_cipher = forms.ChoiceField( + choices=WirelessAuthCipherChoices, + required=False + ) + auth_psk = forms.CharField( + required=False + ) class Meta: - nullable_fields = ['ssid', 'description'] + nullable_fields = ['ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk'] diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 6b22728f62f..e9e9afed635 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -3,6 +3,7 @@ from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField +from wireless.choices import * from wireless.models import * __all__ = ( @@ -38,10 +39,20 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm): to_field_name='name', help_text='Bridged VLAN' ) + auth_type = CSVChoiceField( + choices=WirelessAuthTypeChoices, + required=False, + help_text='Authentication type' + ) + auth_cipher = CSVChoiceField( + choices=WirelessAuthCipherChoices, + required=False, + help_text='Authentication cipher' + ) class Meta: model = WirelessLAN - fields = ('ssid', 'group', 'description', 'vlan') + fields = ('ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk') class WirelessLinkCSVForm(CustomFieldModelCSVForm): @@ -55,7 +66,17 @@ class WirelessLinkCSVForm(CustomFieldModelCSVForm): interface_b = CSVModelChoiceField( queryset=Interface.objects.all() ) + auth_type = CSVChoiceField( + choices=WirelessAuthTypeChoices, + required=False, + help_text='Authentication type' + ) + auth_cipher = CSVChoiceField( + choices=WirelessAuthCipherChoices, + required=False, + help_text='Authentication cipher' + ) class Meta: model = WirelessLink - fields = ('interface_a', 'interface_b', 'ssid', 'description') + fields = ('interface_a', 'interface_b', 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk') diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 13aae99a5f3..483d74a7c3a 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -6,6 +6,7 @@ from utilities.forms import ( add_blank_choice, BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelect, TagFilterField, ) +from wireless.choices import * from wireless.models import * __all__ = ( @@ -52,6 +53,19 @@ class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm): label=_('Group'), fetch_trigger='open' ) + auth_type = forms.ChoiceField( + required=False, + choices=add_blank_choice(WirelessAuthTypeChoices), + widget=StaticSelect() + ) + auth_cipher = forms.ChoiceField( + required=False, + choices=add_blank_choice(WirelessAuthCipherChoices), + widget=StaticSelect() + ) + auth_psk = forms.CharField( + required=False + ) tag = TagFilterField(model) @@ -74,4 +88,17 @@ class WirelessLinkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): choices=add_blank_choice(LinkStatusChoices), widget=StaticSelect() ) + auth_type = forms.ChoiceField( + required=False, + choices=add_blank_choice(WirelessAuthTypeChoices), + widget=StaticSelect() + ) + auth_cipher = forms.ChoiceField( + required=False, + choices=add_blank_choice(WirelessAuthCipherChoices), + widget=StaticSelect() + ) + auth_psk = forms.CharField( + required=False + ) tag = TagFilterField(model) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 9a7b78b31a5..aa453ba64f6 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -35,7 +35,8 @@ class WirelessLANForm(BootstrapMixin, CustomFieldModelForm): ) vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), - required=False + required=False, + label='VLAN' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -45,12 +46,17 @@ class WirelessLANForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLAN fields = [ - 'ssid', 'group', 'description', 'vlan', 'tags', + 'ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', ] fieldsets = ( ('Wireless LAN', ('ssid', 'group', 'description', 'tags')), ('VLAN', ('vlan',)), + ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) + widgets = { + 'auth_type': StaticSelect, + 'auth_cipher': StaticSelect, + } class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): @@ -94,8 +100,15 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLink fields = [ - 'device_a', 'interface_a', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'tags', + 'device_a', 'interface_a', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'auth_type', + 'auth_cipher', 'auth_psk', 'tags', ] + fieldsets = ( + ('Link', ('device_a', 'interface_a', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'tags')), + ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), + ) widgets = { 'status': StaticSelect, + 'auth_type': StaticSelect, + 'auth_cipher': StaticSelect, } diff --git a/netbox/wireless/migrations/0002_wireless_auth.py b/netbox/wireless/migrations/0002_wireless_auth.py new file mode 100644 index 00000000000..9ca4e351cd8 --- /dev/null +++ b/netbox/wireless/migrations/0002_wireless_auth.py @@ -0,0 +1,41 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wireless', '0001_wireless'), + ] + + operations = [ + migrations.AddField( + model_name='wirelesslan', + name='auth_cipher', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='wirelesslan', + name='auth_psk', + field=models.CharField(blank=True, max_length=64), + ), + migrations.AddField( + model_name='wirelesslan', + name='auth_type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='wirelesslink', + name='auth_cipher', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='wirelesslink', + name='auth_psk', + field=models.CharField(blank=True, max_length=64), + ), + migrations.AddField( + model_name='wirelesslink', + name='auth_type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index b0cacde15ef..43818279e3a 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -8,7 +8,8 @@ from extras.utils import extras_features from netbox.models import BigIDModel, NestedGroupModel, PrimaryModel from utilities.querysets import RestrictedQuerySet -from .constants import SSID_MAX_LENGTH +from .choices import * +from .constants import * __all__ = ( 'WirelessLAN', @@ -17,6 +18,30 @@ ) +class WirelessAuthenticationBase(models.Model): + """ + Abstract model for attaching attributes related to wireless authentication. + """ + auth_type = models.CharField( + max_length=50, + choices=WirelessAuthTypeChoices, + blank=True + ) + auth_cipher = models.CharField( + max_length=50, + choices=WirelessAuthCipherChoices, + blank=True + ) + auth_psk = models.CharField( + max_length=PSK_MAX_LENGTH, + blank=True, + verbose_name='Pre-shared key' + ) + + class Meta: + abstract = True + + @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class WirelessLANGroup(NestedGroupModel): """ @@ -49,12 +74,15 @@ class Meta: ('parent', 'name') ) + def __str__(self): + return self.name + def get_absolute_url(self): return reverse('wireless:wirelesslangroup', args=[self.pk]) @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class WirelessLAN(PrimaryModel): +class WirelessLAN(WirelessAuthenticationBase, PrimaryModel): """ A wireless network formed among an arbitrary number of access point and clients. """ @@ -95,7 +123,7 @@ def get_absolute_url(self): @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') -class WirelessLink(PrimaryModel): +class WirelessLink(WirelessAuthenticationBase, PrimaryModel): """ A point-to-point connection between two wireless Interfaces. """ diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 486fa2a7126..ec8f3ddd2c2 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -48,8 +48,11 @@ class WirelessLANTable(BaseTable): class Meta(BaseTable.Meta): model = WirelessLAN - fields = ('pk', 'ssid', 'group', 'description', 'vlan', 'interface_count', 'tags') - default_columns = ('pk', 'ssid', 'group', 'description', 'vlan', 'interface_count') + fields = ( + 'pk', 'ssid', 'group', 'description', 'vlan', 'interface_count', 'auth_type', 'auth_cipher', 'auth_psk', + 'tags', + ) + default_columns = ('pk', 'ssid', 'group', 'description', 'vlan', 'auth_type', 'interface_count') class WirelessLANInterfacesTable(BaseTable): @@ -94,7 +97,11 @@ class WirelessLinkTable(BaseTable): class Meta(BaseTable.Meta): model = WirelessLink - fields = ('pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'description') - default_columns = ( + fields = ( 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'description', + 'auth_type', 'auth_cipher', 'auth_psk', 'tags', + ) + default_columns = ( + 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'auth_type', + 'description', ) From 4a7159389ee7cb5713e5749cc92bd0f133206ffc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 11:22:56 -0400 Subject: [PATCH 26/28] Add wireless documentation --- docs/core-functionality/wireless.md | 8 ++++++++ docs/models/dcim/interface.md | 11 +++++++++++ docs/models/wireless/wirelesslan.md | 11 +++++++++++ docs/models/wireless/wirelesslangroup.md | 3 +++ docs/models/wireless/wirelesslink.md | 9 +++++++++ mkdocs.yml | 1 + 6 files changed, 43 insertions(+) create mode 100644 docs/core-functionality/wireless.md create mode 100644 docs/models/wireless/wirelesslan.md create mode 100644 docs/models/wireless/wirelesslangroup.md create mode 100644 docs/models/wireless/wirelesslink.md diff --git a/docs/core-functionality/wireless.md b/docs/core-functionality/wireless.md new file mode 100644 index 00000000000..57133f756c0 --- /dev/null +++ b/docs/core-functionality/wireless.md @@ -0,0 +1,8 @@ +# Wireless Networks + +{!models/wireless/wirelesslan.md!} +{!models/wireless/wirelesslangroup.md!} + +--- + +{!models/wireless/wirelesslink.md!} diff --git a/docs/models/dcim/interface.md b/docs/models/dcim/interface.md index bd9975a72f3..585674de1ac 100644 --- a/docs/models/dcim/interface.md +++ b/docs/models/dcim/interface.md @@ -11,6 +11,17 @@ Interfaces may be physical or virtual in nature, but only physical interfaces ma Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically. +### Wireless Interfaces + +Wireless interfaces may additionally track the following attributes: + +* **Role** - AP or station +* **Channel** - One of several standard wireless channels +* **Channel Frequency** - The transmit frequency +* **Channel Width** - Channel bandwidth + +If a predefined channel is selected, the frequency and width attributes will be assigned automatically. If no channel is selected, these attributes may be defined manually. + ### IP Address Assignment IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.) diff --git a/docs/models/wireless/wirelesslan.md b/docs/models/wireless/wirelesslan.md new file mode 100644 index 00000000000..80a3a40b0df --- /dev/null +++ b/docs/models/wireless/wirelesslan.md @@ -0,0 +1,11 @@ +# Wireless LANs + +A wireless LAN is a set of interfaces connected via a common wireless channel. Each instance must have an SSID, and may optionally be correlated to a VLAN. Wireless LANs can be arranged into hierarchical groups. + +An interface may be attached to multiple wireless LANs, provided they are all operating on the same channel. Only wireless interfaces may be attached to wireless LANs. + +Each wireless LAN may have authentication attributes associated with it, including: + +* Authentication type +* Cipher +* Pre-shared key diff --git a/docs/models/wireless/wirelesslangroup.md b/docs/models/wireless/wirelesslangroup.md new file mode 100644 index 00000000000..e477abd0b75 --- /dev/null +++ b/docs/models/wireless/wirelesslangroup.md @@ -0,0 +1,3 @@ +# Wireless LAN Groups + +Wireless LAN groups can be used to organize and classify wireless LANs. These groups are hierarchical: groups can be nested within parent groups. However, each wireless LAN may assigned only to one group. diff --git a/docs/models/wireless/wirelesslink.md b/docs/models/wireless/wirelesslink.md new file mode 100644 index 00000000000..85cdbd6d93d --- /dev/null +++ b/docs/models/wireless/wirelesslink.md @@ -0,0 +1,9 @@ +# Wireless Links + +A wireless link represents a connection between exactly two wireless interfaces. It may optionally be assigned an SSID and a description. It may also have a status assigned to it, similar to the cable model. + +Each wireless link may have authentication attributes associated with it, including: + +* Authentication type +* Cipher +* Pre-shared key diff --git a/mkdocs.yml b/mkdocs.yml index 7244c36d6f2..ac394d704bf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -60,6 +60,7 @@ nav: - Virtualization: 'core-functionality/virtualization.md' - Service Mapping: 'core-functionality/services.md' - Circuits: 'core-functionality/circuits.md' + - Wireless: 'core-functionality/wireless.md' - Power Tracking: 'core-functionality/power.md' - Tenancy: 'core-functionality/tenancy.md' - Customization: From 6a4becfb4602675429fc75a1c511c35d7415ae68 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 13:34:39 -0400 Subject: [PATCH 27/28] Add tests for wireless --- netbox/dcim/filtersets.py | 9 +- netbox/dcim/tests/test_filtersets.py | 27 +++- netbox/wireless/api/serializers.py | 6 +- netbox/wireless/filtersets.py | 17 +- netbox/wireless/forms/bulk_import.py | 1 + netbox/wireless/forms/models.py | 2 + netbox/wireless/graphql/schema.py | 12 +- netbox/wireless/graphql/types.py | 18 ++- netbox/wireless/signals.py | 1 - netbox/wireless/tests/__init__.py | 0 netbox/wireless/tests/test_api.py | 141 ++++++++++++++++ netbox/wireless/tests/test_filtersets.py | 194 +++++++++++++++++++++++ netbox/wireless/tests/test_views.py | 120 ++++++++++++++ 13 files changed, 529 insertions(+), 19 deletions(-) create mode 100644 netbox/wireless/tests/__init__.py create mode 100644 netbox/wireless/tests/test_api.py create mode 100644 netbox/wireless/tests/test_filtersets.py create mode 100644 netbox/wireless/tests/test_views.py diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index aa525e8e18c..f6d6ed8dccd 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -14,6 +14,7 @@ TreeNodeMultipleChoiceFilter, ) from virtualization.models import Cluster +from wireless.choices import WirelessRoleChoices, WirelessChannelChoices from .choices import * from .constants import * from .models import * @@ -987,12 +988,18 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT choices=InterfaceTypeChoices, null_value=None ) + rf_role = django_filters.MultipleChoiceFilter( + choices=WirelessRoleChoices + ) + rf_channel = django_filters.MultipleChoiceFilter( + choices=WirelessChannelChoices + ) class Meta: model = Interface fields = [ 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel', - 'rf_channel_width', 'description', + 'rf_channel_frequency', 'rf_channel_width', 'description', ] def filter_device(self, queryset, name, value): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 8d8be324e58..62bdaed82cd 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -9,6 +9,7 @@ from utilities.choices import ColorChoices from utilities.testing import ChangeLoggedFilterSetTests from virtualization.models import Cluster, ClusterType +from wireless.choices import WirelessChannelChoices, WirelessRoleChoices class RegionTestCase(TestCase, ChangeLoggedFilterSetTests): @@ -2063,6 +2064,8 @@ def setUpTestData(cls): Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), + Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22), + Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_STATION, rf_channel=WirelessChannelChoices.CHANNEL_5G_32, rf_channel_frequency=5160, rf_channel_width=20), ) Interface.objects.bulk_create(interfaces) @@ -2083,11 +2086,11 @@ def test_connected(self): params = {'connected': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'connected': False} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_enabled(self): params = {'enabled': 'true'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'enabled': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2099,7 +2102,7 @@ def test_mgmt_only(self): params = {'mgmt_only': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'mgmt_only': 'false'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_mode(self): params = {'mode': InterfaceModeChoices.MODE_ACCESS} @@ -2176,7 +2179,7 @@ def test_cabled(self): params = {'cabled': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'cabled': 'false'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_kind(self): params = {'kind': 'physical'} @@ -2192,6 +2195,22 @@ def test_type(self): params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_rf_role(self): + params = {'rf_role': [WirelessRoleChoices.ROLE_AP, WirelessRoleChoices.ROLE_STATION]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_rf_channel(self): + params = {'rf_channel': [WirelessChannelChoices.CHANNEL_24G_1, WirelessChannelChoices.CHANNEL_5G_32]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_rf_channel_frequency(self): + params = {'rf_channel_frequency': [2412, 5160]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_rf_channel_width(self): + params = {'rf_channel_width': [22, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FrontPort.objects.all() diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index e9be3561859..12986dcaf1b 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -10,6 +10,7 @@ from .nested_serializers import * __all__ = ( + 'WirelessLANGroupSerializer', 'WirelessLANSerializer', 'WirelessLinkSerializer', ) @@ -17,7 +18,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') - parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True) + parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None) wirelesslan_count = serializers.IntegerField(read_only=True) class Meta: @@ -30,6 +31,7 @@ class Meta: class WirelessLANSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') + group = NestedWirelessLANGroupSerializer(required=False, allow_null=True) vlan = NestedVLANSerializer(required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) @@ -37,7 +39,7 @@ class WirelessLANSerializer(PrimaryModelSerializer): class Meta: model = WirelessLAN fields = [ - 'id', 'url', 'display', 'ssid', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', + 'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', ] diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index cc67c1fc39c..cffdcf0466a 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -3,7 +3,9 @@ from dcim.choices import LinkStatusChoices from extras.filters import TagFilter +from ipam.models import VLAN from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet +from utilities.filters import TreeNodeMultipleChoiceFilter from .choices import * from .models import * @@ -34,8 +36,19 @@ class WirelessLANFilterSet(PrimaryModelFilterSet): method='search', label='Search', ) - group_id = django_filters.ModelMultipleChoiceFilter( - queryset=WirelessLANGroup.objects.all() + group_id = TreeNodeMultipleChoiceFilter( + queryset=WirelessLANGroup.objects.all(), + field_name='group', + lookup_expr='in' + ) + group = TreeNodeMultipleChoiceFilter( + queryset=WirelessLANGroup.objects.all(), + field_name='group', + lookup_expr='in', + to_field_name='slug' + ) + vlan_id = django_filters.ModelMultipleChoiceFilter( + queryset=VLAN.objects.all() ) auth_type = django_filters.MultipleChoiceFilter( choices=WirelessAuthTypeChoices diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index e9e9afed635..aa79e1fc713 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -36,6 +36,7 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm): ) vlan = CSVModelChoiceField( queryset=VLAN.objects.all(), + required=False, to_field_name='name', help_text='Bridged VLAN' ) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index aa453ba64f6..26bcd22607f 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -62,6 +62,7 @@ class Meta: class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): device_a = DynamicModelChoiceField( queryset=Device.objects.all(), + required=False, label='Device A', initial_params={ 'interfaces': '$interface_a' @@ -78,6 +79,7 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): ) device_b = DynamicModelChoiceField( queryset=Device.objects.all(), + required=False, label='Device B', initial_params={ 'interfaces': '$interface_b' diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py index 05fc57c4d31..cd8fd9f5267 100644 --- a/netbox/wireless/graphql/schema.py +++ b/netbox/wireless/graphql/schema.py @@ -5,11 +5,11 @@ class WirelessQuery(graphene.ObjectType): - wirelesslan = ObjectField(WirelessLANType) - wirelesslan_list = ObjectListField(WirelessLANType) + wireless_lan = ObjectField(WirelessLANType) + wireless_lan_list = ObjectListField(WirelessLANType) - wirelesslangroup = ObjectField(WirelessLANGroupType) - wirelesslangroup_list = ObjectListField(WirelessLANGroupType) + wireless_lan_group = ObjectField(WirelessLANGroupType) + wireless_lan_group_list = ObjectListField(WirelessLANGroupType) - wirelesslink = ObjectField(WirelessLinkType) - wirelesslink_list = ObjectListField(WirelessLinkType) + wireless_link = ObjectField(WirelessLinkType) + wireless_link_list = ObjectListField(WirelessLinkType) diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 4697cc44b59..be0b2f7aa12 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -1,5 +1,5 @@ from wireless import filtersets, models -from netbox.graphql.types import ObjectType +from netbox.graphql.types import ObjectType, PrimaryObjectType __all__ = ( 'WirelessLANType', @@ -16,17 +16,29 @@ class Meta: filterset_class = filtersets.WirelessLANGroupFilterSet -class WirelessLANType(ObjectType): +class WirelessLANType(PrimaryObjectType): class Meta: model = models.WirelessLAN fields = '__all__' filterset_class = filtersets.WirelessLANFilterSet + def resolve_auth_type(self, info): + return self.auth_type or None -class WirelessLinkType(ObjectType): + def resolve_auth_cipher(self, info): + return self.auth_cipher or None + + +class WirelessLinkType(PrimaryObjectType): class Meta: model = models.WirelessLink fields = '__all__' filterset_class = filtersets.WirelessLinkFilterSet + + def resolve_auth_type(self, info): + return self.auth_type or None + + def resolve_auth_cipher(self, info): + return self.auth_cipher or None diff --git a/netbox/wireless/signals.py b/netbox/wireless/signals.py index 935e116771b..3b4831a8d68 100644 --- a/netbox/wireless/signals.py +++ b/netbox/wireless/signals.py @@ -63,5 +63,4 @@ def nullify_connected_interfaces(instance, **kwargs): # Delete and retrace any dependent cable paths for cablepath in CablePath.objects.filter(path__contains=instance): - print(f'Deleting cable path {cablepath.pk}') cablepath.delete() diff --git a/netbox/wireless/tests/__init__.py b/netbox/wireless/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/netbox/wireless/tests/test_api.py b/netbox/wireless/tests/test_api.py new file mode 100644 index 00000000000..917b7b320a3 --- /dev/null +++ b/netbox/wireless/tests/test_api.py @@ -0,0 +1,141 @@ +from django.urls import reverse + +from wireless.choices import * +from wireless.models import * +from dcim.choices import InterfaceTypeChoices +from dcim.models import Interface +from utilities.testing import APITestCase, APIViewTestCases, create_test_device + + +class AppTest(APITestCase): + + def test_root(self): + url = reverse('wireless-api:api-root') + response = self.client.get('{}?format=api'.format(url), **self.header) + + self.assertEqual(response.status_code, 200) + + +class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase): + model = WirelessLANGroup + brief_fields = ['_depth', 'display', 'id', 'name', 'slug', 'url', 'wirelesslan_count'] + create_data = [ + { + 'name': 'Wireless LAN Group 4', + 'slug': 'wireless-lan-group-4', + }, + { + 'name': 'Wireless LAN Group 5', + 'slug': 'wireless-lan-group-5', + }, + { + 'name': 'Wireless LAN Group 6', + 'slug': 'wireless-lan-group-6', + }, + ] + bulk_update_data = { + 'description': 'New description', + } + + @classmethod + def setUpTestData(cls): + + WirelessLANGroup.objects.create(name='Wireless LAN Group 1', slug='wireless-lan-group-1') + WirelessLANGroup.objects.create(name='Wireless LAN Group 2', slug='wireless-lan-group-2') + WirelessLANGroup.objects.create(name='Wireless LAN Group 3', slug='wireless-lan-group-3') + + +class WirelessLANTest(APIViewTestCases.APIViewTestCase): + model = WirelessLAN + brief_fields = ['display', 'id', 'ssid', 'url'] + + @classmethod + def setUpTestData(cls): + + groups = ( + WirelessLANGroup(name='Group 1', slug='group-1'), + WirelessLANGroup(name='Group 2', slug='group-2'), + WirelessLANGroup(name='Group 3', slug='group-3'), + ) + for group in groups: + group.save() + + wireless_lans = ( + WirelessLAN(ssid='WLAN1'), + WirelessLAN(ssid='WLAN2'), + WirelessLAN(ssid='WLAN3'), + ) + WirelessLAN.objects.bulk_create(wireless_lans) + + cls.create_data = [ + { + 'ssid': 'WLAN4', + 'group': groups[0].pk, + 'auth_type': WirelessAuthTypeChoices.TYPE_OPEN, + }, + { + 'ssid': 'WLAN5', + 'group': groups[1].pk, + 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, + }, + { + 'ssid': 'WLAN6', + 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE, + }, + ] + + cls.bulk_update_data = { + 'group': groups[2].pk, + 'description': 'New description', + 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, + 'auth_cipher': WirelessAuthCipherChoices.CIPHER_AES, + 'auth_psk': 'abc123def456', + } + + +class WirelessLinkTest(APIViewTestCases.APIViewTestCase): + model = WirelessLink + brief_fields = ['display', 'id', 'ssid', 'url'] + bulk_update_data = { + 'status': 'planned', + } + + @classmethod + def setUpTestData(cls): + device = create_test_device('test-device') + interfaces = [ + Interface( + device=device, + name=f'radio{i}', + type=InterfaceTypeChoices.TYPE_80211AC, + rf_channel=WirelessChannelChoices.CHANNEL_5G_32, + rf_channel_frequency=5160, + rf_channel_width=20 + ) for i in range(12) + ] + Interface.objects.bulk_create(interfaces) + + wireless_links = ( + WirelessLink(ssid='LINK1', interface_a=interfaces[0], interface_b=interfaces[1]), + WirelessLink(ssid='LINK2', interface_a=interfaces[2], interface_b=interfaces[3]), + WirelessLink(ssid='LINK3', interface_a=interfaces[4], interface_b=interfaces[5]), + ) + WirelessLink.objects.bulk_create(wireless_links) + + cls.create_data = [ + { + 'interface_a': interfaces[6].pk, + 'interface_b': interfaces[7].pk, + 'ssid': 'LINK4', + }, + { + 'interface_a': interfaces[8].pk, + 'interface_b': interfaces[9].pk, + 'ssid': 'LINK5', + }, + { + 'interface_a': interfaces[10].pk, + 'interface_b': interfaces[11].pk, + 'ssid': 'LINK6', + }, + ] diff --git a/netbox/wireless/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py new file mode 100644 index 00000000000..50f89c4d65b --- /dev/null +++ b/netbox/wireless/tests/test_filtersets.py @@ -0,0 +1,194 @@ +from django.test import TestCase + +from dcim.choices import InterfaceTypeChoices, LinkStatusChoices +from dcim.models import Interface +from ipam.models import VLAN +from wireless.choices import * +from wireless.filtersets import * +from wireless.models import * +from utilities.testing import ChangeLoggedFilterSetTests, create_test_device + + +class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = WirelessLANGroup.objects.all() + filterset = WirelessLANGroupFilterSet + + @classmethod + def setUpTestData(cls): + + groups = ( + WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1', description='A'), + WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2', description='B'), + WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3', description='C'), + ) + for group in groups: + group.save() + + child_groups = ( + WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=groups[0]), + WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=groups[0]), + WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=groups[1]), + WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=groups[1]), + WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=groups[2]), + WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=groups[2]), + ) + for group in child_groups: + group.save() + + def test_name(self): + params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_slug(self): + params = {'slug': ['wireless-lan-group-1', 'wireless-lan-group-2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_parent(self): + parent_groups = WirelessLANGroup.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + +class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = WirelessLAN.objects.all() + filterset = WirelessLANFilterSet + + @classmethod + 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'), + ) + for group in groups: + group.save() + + vlans = ( + VLAN(name='VLAN1', vid=1), + VLAN(name='VLAN2', vid=2), + VLAN(name='VLAN3', vid=3), + ) + VLAN.objects.bulk_create(vlans) + + wireless_lans = ( + WirelessLAN(ssid='WLAN1', group=groups[0], vlan=vlans[0], auth_type=WirelessAuthTypeChoices.TYPE_OPEN, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_psk='PSK1'), + WirelessLAN(ssid='WLAN2', group=groups[1], vlan=vlans[1], auth_type=WirelessAuthTypeChoices.TYPE_WEP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_psk='PSK2'), + WirelessLAN(ssid='WLAN3', group=groups[2], vlan=vlans[2], auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, auth_psk='PSK3'), + ) + WirelessLAN.objects.bulk_create(wireless_lans) + + def test_ssid(self): + params = {'ssid': ['WLAN1', 'WLAN2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_group(self): + groups = WirelessLANGroup.objects.all()[:2] + params = {'group_id': [groups[0].pk, groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'group': [groups[0].slug, groups[1].slug]} + 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]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_auth_type(self): + params = {'auth_type': [WirelessAuthTypeChoices.TYPE_OPEN, WirelessAuthTypeChoices.TYPE_WEP]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_auth_cipher(self): + params = {'auth_cipher': [WirelessAuthCipherChoices.CIPHER_AUTO, WirelessAuthCipherChoices.CIPHER_TKIP]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_auth_psk(self): + params = {'auth_psk': ['PSK1', 'PSK2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = WirelessLink.objects.all() + filterset = WirelessLinkFilterSet + + @classmethod + def setUpTestData(cls): + + devices = ( + create_test_device('device1'), + create_test_device('device2'), + create_test_device('device3'), + create_test_device('device4'), + ) + + interfaces = ( + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC), + Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC), + ) + Interface.objects.bulk_create(interfaces) + + # Wireless links + WirelessLink( + interface_a=interfaces[0], + interface_b=interfaces[2], + ssid='LINK1', + status=LinkStatusChoices.STATUS_CONNECTED, + auth_type=WirelessAuthTypeChoices.TYPE_OPEN, + auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, + auth_psk='PSK1' + ).save() + WirelessLink( + interface_a=interfaces[1], + interface_b=interfaces[3], + ssid='LINK2', + status=LinkStatusChoices.STATUS_PLANNED, + auth_type=WirelessAuthTypeChoices.TYPE_WEP, + auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, + auth_psk='PSK2' + ).save() + WirelessLink( + interface_a=interfaces[4], + interface_b=interfaces[6], + ssid='LINK3', + status=LinkStatusChoices.STATUS_DECOMMISSIONING, + auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, + auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, + auth_psk='PSK3' + ).save() + WirelessLink( + interface_a=interfaces[5], + interface_b=interfaces[7], + ssid='LINK4' + ).save() + + def test_ssid(self): + params = {'ssid': ['LINK1', 'LINK2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_status(self): + params = {'status': [LinkStatusChoices.STATUS_PLANNED, LinkStatusChoices.STATUS_DECOMMISSIONING]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_auth_type(self): + params = {'auth_type': [WirelessAuthTypeChoices.TYPE_OPEN, WirelessAuthTypeChoices.TYPE_WEP]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_auth_cipher(self): + params = {'auth_cipher': [WirelessAuthCipherChoices.CIPHER_AUTO, WirelessAuthCipherChoices.CIPHER_TKIP]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_auth_psk(self): + params = {'auth_psk': ['PSK1', 'PSK2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/wireless/tests/test_views.py b/netbox/wireless/tests/test_views.py new file mode 100644 index 00000000000..d4422e7e30c --- /dev/null +++ b/netbox/wireless/tests/test_views.py @@ -0,0 +1,120 @@ +from wireless.choices import * +from wireless.models import * +from dcim.choices import InterfaceTypeChoices, LinkStatusChoices +from dcim.models import Interface +from utilities.testing import ViewTestCases, create_tags, create_test_device + + +class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): + model = WirelessLANGroup + + @classmethod + 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'), + ) + for group in groups: + group.save() + + cls.form_data = { + 'name': 'Wireless LAN Group X', + 'slug': 'wireless-lan-group-x', + 'parent': groups[2].pk, + 'description': 'A new wireless LAN group', + } + + cls.csv_data = ( + "name,slug,description", + "Wireles sLAN Group 4,wireless-lan-group-4,Fourth wireless LAN group", + "Wireless LAN Group 5,wireless-lan-group-5,Fifth wireless LAN group", + "Wireless LAN Group 6,wireless-lan-group-6,Sixth wireless LAN group", + ) + + cls.bulk_edit_data = { + 'description': 'New description', + } + + +class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = WirelessLAN + + @classmethod + 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'), + ) + for group in groups: + group.save() + + WirelessLAN.objects.bulk_create([ + WirelessLAN(group=groups[0], ssid='WLAN1'), + WirelessLAN(group=groups[0], ssid='WLAN2'), + WirelessLAN(group=groups[0], ssid='WLAN3'), + ]) + + tags = create_tags('Alpha', 'Bravo', 'Charlie') + + cls.form_data = { + 'ssid': 'WLAN2', + 'group': groups[1].pk, + 'tags': [t.pk for t in tags], + } + + cls.csv_data = ( + "group,ssid", + "Wireless LAN Group 2,WLAN4", + "Wireless LAN Group 2,WLAN5", + "Wireless LAN Group 2,WLAN6", + ) + + cls.bulk_edit_data = { + 'description': 'New description', + } + + +class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = WirelessLink + + @classmethod + def setUpTestData(cls): + device = create_test_device('test-device') + interfaces = [ + Interface( + device=device, + name=f'radio{i}', + type=InterfaceTypeChoices.TYPE_80211AC, + rf_channel=WirelessChannelChoices.CHANNEL_5G_32, + rf_channel_frequency=5160, + rf_channel_width=20 + ) for i in range(12) + ] + Interface.objects.bulk_create(interfaces) + + WirelessLink(interface_a=interfaces[0], interface_b=interfaces[1], ssid='LINK1').save() + WirelessLink(interface_a=interfaces[2], interface_b=interfaces[3], ssid='LINK2').save() + WirelessLink(interface_a=interfaces[4], interface_b=interfaces[5], ssid='LINK3').save() + + tags = create_tags('Alpha', 'Bravo', 'Charlie') + + cls.form_data = { + 'interface_a': interfaces[6].pk, + 'interface_b': interfaces[7].pk, + 'status': LinkStatusChoices.STATUS_PLANNED, + 'tags': [t.pk for t in tags], + } + + cls.csv_data = ( + "interface_a,interface_b,status", + f"{interfaces[6].pk},{interfaces[7].pk},connected", + f"{interfaces[8].pk},{interfaces[9].pk},connected", + f"{interfaces[10].pk},{interfaces[11].pk},connected", + ) + + cls.bulk_edit_data = { + 'status': LinkStatusChoices.STATUS_PLANNED, + } From 1c6a84659cc0a401b43f2abae8334fbf8eaf5774 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 21 Oct 2021 14:11:11 -0400 Subject: [PATCH 28/28] #3979 cleanup --- netbox/dcim/tables/template_code.py | 4 +- netbox/netbox/views/__init__.py | 8 ++- .../templates/wireless/wirelesslink_edit.html | 33 ++++++++++ netbox/wireless/forms/models.py | 62 ++++++++++++++++--- 4 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 netbox/templates/wireless/wirelesslink_edit.html diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index aab15b5efcf..f6938807a7c 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -236,8 +236,8 @@
{% endif %} {% elif record.is_wireless and perms.wireless.add_wirelesslink %} - - + + {% endif %} """ diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index 2c033e76019..b361352d086 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -27,6 +27,7 @@ from netbox.forms import SearchForm from tenancy.models import Tenant from virtualization.models import Cluster, VirtualMachine +from wireless.models import WirelessLAN, WirelessLink class HomeView(View): @@ -92,14 +93,19 @@ def build_stats(): ("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count), ("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count), ) + wireless = ( + ("wireless.view_wirelesslan", "Wireless LANs", WirelessLAN.objects.restrict(request.user, 'view').count), + ("wireless.view_wirelesslink", "Wireless Links", WirelessLink.objects.restrict(request.user, 'view').count), + ) sections = ( ("Organization", org, "domain"), ("IPAM", ipam, "counter"), ("Virtualization", virtualization, "monitor"), ("Inventory", dcim, "server"), - ("Connections", connections, "cable-data"), ("Circuits", circuits, "transit-connection-variant"), + ("Connections", connections, "cable-data"), ("Power", power, "flash"), + ("Wireless", wireless, "wifi"), ) stats = [] diff --git a/netbox/templates/wireless/wirelesslink_edit.html b/netbox/templates/wireless/wirelesslink_edit.html new file mode 100644 index 00000000000..034d147decc --- /dev/null +++ b/netbox/templates/wireless/wirelesslink_edit.html @@ -0,0 +1,33 @@ +{% extends 'generic/object_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
+
+
+
Side A
+
+ {% render_field form.device_a %} + {% render_field form.interface_a %} +
+
+
+
+
+
Side B
+
+ {% render_field form.device_b %} + {% render_field form.interface_b %} +
+
+
+ {% if form.custom_fields %} +
+
+
Custom Fields
+
+ {% render_custom_fields form %} +
+ {% endif %} +{% endblock %} diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 544d5823db0..f7985a31d76 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -1,4 +1,4 @@ -from dcim.models import Device, Interface +from dcim.models import Device, Interface, Location, Site from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN @@ -64,10 +64,30 @@ class Meta: class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): + site_a = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + label='Site', + initial_params={ + 'devices': '$device_a', + } + ) + location_a = DynamicModelChoiceField( + queryset=Location.objects.all(), + required=False, + label='Location', + initial_params={ + 'devices': '$device_a', + } + ) device_a = DynamicModelChoiceField( queryset=Device.objects.all(), + query_params={ + 'site_id': '$site_a', + 'location_id': '$location_a', + }, required=False, - label='Device A', + label='Device', initial_params={ 'interfaces': '$interface_a' } @@ -79,12 +99,32 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): 'device_id': '$device_a', }, disabled_indicator='_occupied', - label='Interface A' + label='Interface' + ) + site_b = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False, + label='Site', + initial_params={ + 'devices': '$device_b', + } + ) + location_b = DynamicModelChoiceField( + queryset=Location.objects.all(), + required=False, + label='Location', + initial_params={ + 'devices': '$device_b', + } ) device_b = DynamicModelChoiceField( queryset=Device.objects.all(), + query_params={ + 'site_id': '$site_b', + 'location_id': '$location_b', + }, required=False, - label='Device B', + label='Device', initial_params={ 'interfaces': '$interface_b' } @@ -96,7 +136,7 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): 'device_id': '$device_b', }, disabled_indicator='_occupied', - label='Interface B' + label='Interface' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -106,11 +146,13 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLink fields = [ - 'device_a', 'interface_a', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'auth_type', - 'auth_cipher', 'auth_psk', 'tags', + 'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b', + 'status', 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', ] fieldsets = ( - ('Link', ('device_a', 'interface_a', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'tags')), + ('Side A', ('site_a', 'location_a', 'device_a', 'interface_a')), + ('Side B', ('site_b', 'location_b', 'device_b', 'interface_b')), + ('Link', ('status', 'ssid', 'description', 'tags')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) widgets = { @@ -118,3 +160,7 @@ class Meta: 'auth_type': StaticSelect, 'auth_cipher': StaticSelect, } + labels = { + 'auth_type': 'Type', + 'auth_cipher': 'Cipher', + }