From 7dd5f9e720cc06a06462952a0c619b86c49478af Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Jun 2022 11:30:52 -0400 Subject: [PATCH] Closes #9177: Add tenant assignment for wireless LANs & links --- docs/models/wireless/wirelesslan.md | 2 +- docs/models/wireless/wirelesslink.md | 2 +- docs/release-notes/version-3.3.md | 5 ++ netbox/templates/tenancy/tenant.html | 12 +++- netbox/templates/wireless/wirelesslan.html | 67 +++++++++++-------- netbox/templates/wireless/wirelesslink.html | 9 +++ netbox/tenancy/views.py | 3 + netbox/wireless/api/serializers.py | 9 ++- netbox/wireless/api/views.py | 4 +- netbox/wireless/filtersets.py | 5 +- netbox/wireless/forms/bulk_edit.py | 17 +++-- netbox/wireless/forms/bulk_import.py | 19 +++++- netbox/wireless/forms/filtersets.py | 12 +++- netbox/wireless/forms/models.py | 13 ++-- .../migrations/0004_wireless_tenancy.py | 25 +++++++ netbox/wireless/models.py | 14 ++++ netbox/wireless/tables/wirelesslan.py | 6 +- netbox/wireless/tables/wirelesslink.py | 4 +- netbox/wireless/tests/test_api.py | 30 +++++++-- netbox/wireless/tests/test_filtersets.py | 44 +++++++++--- netbox/wireless/tests/test_views.py | 46 +++++++++---- 21 files changed, 265 insertions(+), 83 deletions(-) create mode 100644 netbox/wireless/migrations/0004_wireless_tenancy.py diff --git a/docs/models/wireless/wirelesslan.md b/docs/models/wireless/wirelesslan.md index 80a3a40b0..cb478664c 100644 --- a/docs/models/wireless/wirelesslan.md +++ b/docs/models/wireless/wirelesslan.md @@ -1,6 +1,6 @@ # 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. +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, and each may be associated with a particular tenant. 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. diff --git a/docs/models/wireless/wirelesslink.md b/docs/models/wireless/wirelesslink.md index 85cdbd6d9..f52dc7191 100644 --- a/docs/models/wireless/wirelesslink.md +++ b/docs/models/wireless/wirelesslink.md @@ -1,6 +1,6 @@ # 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. +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 also be assigned to a particular tenant. Each wireless link may have authentication attributes associated with it, including: diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 8deee0370..1e18de1e6 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -28,6 +28,7 @@ * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results * [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields +* [#9177](https://github.com/netbox-community/netbox/issues/9177) - Add tenant assignment for wireless LANs & links * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location @@ -70,3 +71,7 @@ * Added `device` field * The `site` field is now directly writable (rather than being inferred from the assigned cluster) * The `cluster` field is now optional. A virtual machine must have a site and/or cluster assigned. +wireless.WirelessLAN + * Added `tenant` field +wireless.WirelessLink + * Added `tenant` field diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 52c13e1aa..e8dc4b23a 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -61,6 +61,10 @@

{{ stats.device_count }}

Devices

+
+

{{ stats.cable_count }}

+

Cables

+

{{ stats.vrf_count }}

VRFs

@@ -102,8 +106,12 @@

Clusters

-

{{ stats.cable_count }}

-

Cables

+

{{ stats.wirelesslan_count }}

+

Wireless LANs

+
+
+

{{ stats.wirelesslink_count }}

+

Wireless Links

diff --git a/netbox/templates/wireless/wirelesslan.html b/netbox/templates/wireless/wirelesslan.html index 185a44904..9250ef7ef 100644 --- a/netbox/templates/wireless/wirelesslan.html +++ b/netbox/templates/wireless/wirelesslan.html @@ -6,36 +6,45 @@ {% block content %}
-
-
Wireless LAN
-
- - - - - - - - - - - - - - - - - -
SSID{{ object.ssid }}
Group{{ object.group|linkify|placeholder }}
Description{{ object.description|placeholder }}
VLAN{{ object.vlan|linkify|placeholder }}
-
-
- {% include 'inc/panels/tags.html' %} - {% plugin_left_page object %} +
+
Wireless LAN
+
+ + + + + + + + + + + + + + + + + + + + + +
SSID{{ object.ssid }}
Group{{ object.group|linkify|placeholder }}
Description{{ object.description|placeholder }}
VLAN{{ object.vlan|linkify|placeholder }}
Tenant + {% if object.tenant.group %} + {{ object.tenant.group|linkify }} / + {% endif %} + {{ object.tenant|linkify|placeholder }} +
+
-
- {% include 'wireless/inc/authentication_attrs.html' %} - {% include 'inc/panels/custom_fields.html' %} - {% plugin_right_page object %} + {% include 'inc/panels/tags.html' %} + {% plugin_left_page object %} +
+
+ {% include 'wireless/inc/authentication_attrs.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_right_page object %}
diff --git a/netbox/templates/wireless/wirelesslink.html b/netbox/templates/wireless/wirelesslink.html index 4795dcdde..d1a93e40d 100644 --- a/netbox/templates/wireless/wirelesslink.html +++ b/netbox/templates/wireless/wirelesslink.html @@ -23,6 +23,15 @@ SSID {{ object.ssid|placeholder }} + + Tenant + + {% if object.tenant.group %} + {{ object.tenant.group|linkify }} / + {% endif %} + {{ object.tenant|linkify|placeholder }} + + Description {{ object.description|placeholder }} diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index f6f95b123..07a25b5a4 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -7,6 +7,7 @@ from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF, ASN from netbox.views import generic from utilities.utils import count_related from virtualization.models import VirtualMachine, Cluster +from wireless.models import WirelessLAN, WirelessLink from . import filtersets, forms, tables from .models import * @@ -114,6 +115,8 @@ class TenantView(generic.ObjectView): 'cluster_count': Cluster.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'cable_count': Cable.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'asn_count': ASN.objects.restrict(request.user, 'view').filter(tenant=instance).count(), + 'wirelesslan_count': WirelessLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(), + 'wirelesslink_count': WirelessLink.objects.restrict(request.user, 'view').filter(tenant=instance).count(), } return { diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 4a6abe94d..49d512e51 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -5,6 +5,7 @@ from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer from netbox.api import ChoiceField from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer from wireless.choices import * from wireless.models import * from .nested_serializers import * @@ -33,14 +34,15 @@ class WirelessLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') group = NestedWirelessLANGroupSerializer(required=False, allow_null=True) vlan = NestedVLANSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) class Meta: model = WirelessLAN fields = [ - 'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'tenant', 'auth_type', 'auth_cipher', + 'auth_psk', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] @@ -49,12 +51,13 @@ class WirelessLinkSerializer(NetBoxModelSerializer): status = ChoiceField(choices=LinkStatusChoices, required=False) interface_a = NestedInterfaceSerializer() interface_b = NestedInterfaceSerializer() + tenant = NestedTenantSerializer(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 = WirelessLink fields = [ - 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', 'auth_type', + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/wireless/api/views.py b/netbox/wireless/api/views.py index 77a766c50..1103cec37 100644 --- a/netbox/wireless/api/views.py +++ b/netbox/wireless/api/views.py @@ -27,12 +27,12 @@ class WirelessLANGroupViewSet(NetBoxModelViewSet): class WirelessLANViewSet(NetBoxModelViewSet): - queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags') + queryset = WirelessLAN.objects.prefetch_related('vlan', 'tenant', 'tags') serializer_class = serializers.WirelessLANSerializer filterset_class = filtersets.WirelessLANFilterSet class WirelessLinkViewSet(NetBoxModelViewSet): - queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags') + queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tenant', 'tags') serializer_class = serializers.WirelessLinkSerializer filterset_class = filtersets.WirelessLinkFilterSet diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 7b0be857b..60c4f935b 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -4,6 +4,7 @@ from django.db.models import Q from dcim.choices import LinkStatusChoices from ipam.models import VLAN from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet +from tenancy.filtersets import TenancyFilterSet from utilities.filters import MultiValueNumberFilter, TreeNodeMultipleChoiceFilter from .choices import * from .models import * @@ -30,7 +31,7 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): fields = ['id', 'name', 'slug', 'description'] -class WirelessLANFilterSet(NetBoxModelFilterSet): +class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): group_id = TreeNodeMultipleChoiceFilter( queryset=WirelessLANGroup.objects.all(), field_name='group', @@ -66,7 +67,7 @@ class WirelessLANFilterSet(NetBoxModelFilterSet): return queryset.filter(qs_filter) -class WirelessLinkFilterSet(NetBoxModelFilterSet): +class WirelessLinkFilterSet(NetBoxModelFilterSet, TenancyFilterSet): interface_a_id = MultiValueNumberFilter() interface_b_id = MultiValueNumberFilter() status = django_filters.MultipleChoiceFilter( diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 8a472e164..639a1ed1b 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -3,6 +3,7 @@ from django import forms from dcim.choices import LinkStatusChoices from ipam.models import VLAN from netbox.forms import NetBoxModelBulkEditForm +from tenancy.models import Tenant from utilities.forms import add_blank_choice, DynamicModelChoiceField from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH @@ -47,6 +48,10 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): required=False, label='SSID' ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) description = forms.CharField( required=False ) @@ -65,11 +70,11 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLAN fieldsets = ( - (None, ('group', 'vlan', 'ssid', 'description')), + (None, ('group', 'ssid', 'vlan', 'tenant', 'description')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) nullable_fields = ( - 'ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk', + 'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', ) @@ -83,6 +88,10 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): choices=add_blank_choice(LinkStatusChoices), required=False ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) description = forms.CharField( required=False ) @@ -101,9 +110,9 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLink fieldsets = ( - (None, ('ssid', 'status', 'description')), + (None, ('ssid', 'status', 'tenant', 'description')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')) ) nullable_fields = ( - 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk', + 'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', ) diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 4b8acb385..6a1ca4f36 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -2,6 +2,7 @@ from dcim.choices import LinkStatusChoices from dcim.models import Interface from ipam.models import VLAN from netbox.forms import NetBoxModelCSVForm +from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField from wireless.choices import * from wireless.models import * @@ -40,6 +41,12 @@ class WirelessLANCSVForm(NetBoxModelCSVForm): to_field_name='name', help_text='Bridged VLAN' ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) auth_type = CSVChoiceField( choices=WirelessAuthTypeChoices, required=False, @@ -53,7 +60,7 @@ class WirelessLANCSVForm(NetBoxModelCSVForm): class Meta: model = WirelessLAN - fields = ('ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk') + fields = ('ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk') class WirelessLinkCSVForm(NetBoxModelCSVForm): @@ -67,6 +74,12 @@ class WirelessLinkCSVForm(NetBoxModelCSVForm): interface_b = CSVModelChoiceField( queryset=Interface.objects.all() ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) auth_type = CSVChoiceField( choices=WirelessAuthTypeChoices, required=False, @@ -80,4 +93,6 @@ class WirelessLinkCSVForm(NetBoxModelCSVForm): class Meta: model = WirelessLink - fields = ('interface_a', 'interface_b', 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk') + fields = ( + 'interface_a', 'interface_b', 'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', + ) diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 8dcb48673..9e8808e17 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext as _ from dcim.choices import LinkStatusChoices from netbox.forms import NetBoxModelFilterSetForm +from tenancy.forms import TenancyFilterForm from utilities.forms import add_blank_choice, DynamicModelMultipleChoiceField, StaticSelect, TagFilterField from wireless.choices import * from wireless.models import * @@ -24,11 +25,12 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class WirelessLANFilterForm(NetBoxModelFilterSetForm): +class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLAN fieldsets = ( (None, ('q', 'tag')), ('Attributes', ('ssid', 'group_id',)), + ('Tenant', ('tenant_group_id', 'tenant_id')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) ssid = forms.CharField( @@ -57,8 +59,14 @@ class WirelessLANFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class WirelessLinkFilterForm(NetBoxModelFilterSetForm): +class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLink + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('ssid', 'status',)), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), + ) ssid = forms.CharField( required=False, label='SSID' diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index d1012ba59..bcffcf896 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -1,6 +1,7 @@ from dcim.models import Device, Interface, Location, Region, Site, SiteGroup from ipam.models import VLAN, VLANGroup from netbox.forms import NetBoxModelForm +from tenancy.forms import TenancyForm from utilities.forms import DynamicModelChoiceField, SlugField, StaticSelect from wireless.models import * @@ -25,7 +26,7 @@ class WirelessLANGroupForm(NetBoxModelForm): ] -class WirelessLANForm(NetBoxModelForm): +class WirelessLANForm(TenancyForm, NetBoxModelForm): group = DynamicModelChoiceField( queryset=WirelessLANGroup.objects.all(), required=False @@ -79,14 +80,15 @@ class WirelessLANForm(NetBoxModelForm): fieldsets = ( ('Wireless LAN', ('ssid', 'group', 'description', 'tags')), ('VLAN', ('region', 'site_group', 'site', 'vlan_group', 'vlan',)), + ('Tenancy', ('tenant_group', 'tenant')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) class Meta: model = WirelessLAN fields = [ - 'ssid', 'group', 'description', 'region', 'site_group', 'site', 'vlan_group', 'vlan', 'auth_type', - 'auth_cipher', 'auth_psk', 'tags', + 'ssid', 'group', 'description', 'region', 'site_group', 'site', 'vlan_group', 'vlan', 'tenant_group', + 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', ] widgets = { 'auth_type': StaticSelect, @@ -94,7 +96,7 @@ class WirelessLANForm(NetBoxModelForm): } -class WirelessLinkForm(NetBoxModelForm): +class WirelessLinkForm(TenancyForm, NetBoxModelForm): site_a = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, @@ -180,6 +182,7 @@ class WirelessLinkForm(NetBoxModelForm): ('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')), + ('Tenancy', ('tenant_group', 'tenant')), ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), ) @@ -187,7 +190,7 @@ class WirelessLinkForm(NetBoxModelForm): model = WirelessLink fields = [ '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', + 'status', 'ssid', 'tenant_group', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', ] widgets = { 'status': StaticSelect, diff --git a/netbox/wireless/migrations/0004_wireless_tenancy.py b/netbox/wireless/migrations/0004_wireless_tenancy.py new file mode 100644 index 000000000..aa5837b0a --- /dev/null +++ b/netbox/wireless/migrations/0004_wireless_tenancy.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.5 on 2022-06-27 13:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0007_contact_link'), + ('wireless', '0003_created_datetimefield'), + ] + + operations = [ + migrations.AddField( + model_name='wirelesslan', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wireless_lans', to='tenancy.tenant'), + ), + migrations.AddField( + model_name='wirelesslink', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wireless_links', to='tenancy.tenant'), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 0543e5621..dd3945d50 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -101,6 +101,13 @@ class WirelessLAN(WirelessAuthenticationBase, NetBoxModel): null=True, verbose_name='VLAN' ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='wireless_lans', + blank=True, + null=True + ) description = models.CharField( max_length=200, blank=True @@ -143,6 +150,13 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel): choices=LinkStatusChoices, default=LinkStatusChoices.STATUS_CONNECTED ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='wireless_links', + blank=True, + null=True + ) description = models.CharField( max_length=200, blank=True diff --git a/netbox/wireless/tables/wirelesslan.py b/netbox/wireless/tables/wirelesslan.py index 9955d4ac4..fc791d730 100644 --- a/netbox/wireless/tables/wirelesslan.py +++ b/netbox/wireless/tables/wirelesslan.py @@ -2,6 +2,7 @@ import django_tables2 as tables from dcim.models import Interface from netbox.tables import NetBoxTable, columns +from tenancy.tables import TenantColumn from wireless.models import * __all__ = ( @@ -39,6 +40,7 @@ class WirelessLANTable(NetBoxTable): group = tables.Column( linkify=True ) + tenant = TenantColumn() interface_count = tables.Column( verbose_name='Interfaces' ) @@ -49,8 +51,8 @@ class WirelessLANTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = WirelessLAN fields = ( - 'pk', 'ssid', 'group', 'description', 'vlan', 'interface_count', 'auth_type', 'auth_cipher', 'auth_psk', - 'tags', 'created', 'last_updated', + 'pk', 'ssid', 'group', 'tenant', 'description', 'vlan', 'interface_count', 'auth_type', 'auth_cipher', + 'auth_psk', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'ssid', 'group', 'description', 'vlan', 'auth_type', 'interface_count') diff --git a/netbox/wireless/tables/wirelesslink.py b/netbox/wireless/tables/wirelesslink.py index 72037c4d9..6a45a21ae 100644 --- a/netbox/wireless/tables/wirelesslink.py +++ b/netbox/wireless/tables/wirelesslink.py @@ -1,6 +1,7 @@ import django_tables2 as tables from netbox.tables import NetBoxTable, columns +from tenancy.tables import TenantColumn from wireless.models import * __all__ = ( @@ -28,6 +29,7 @@ class WirelessLinkTable(NetBoxTable): interface_b = tables.Column( linkify=True ) + tenant = TenantColumn() tags = columns.TagColumn( url_name='wireless:wirelesslink_list' ) @@ -35,7 +37,7 @@ class WirelessLinkTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = WirelessLink fields = ( - 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'description', + 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', 'created', 'last_updated', ) default_columns = ( diff --git a/netbox/wireless/tests/test_api.py b/netbox/wireless/tests/test_api.py index 917b7b320..9ef552eb7 100644 --- a/netbox/wireless/tests/test_api.py +++ b/netbox/wireless/tests/test_api.py @@ -1,10 +1,11 @@ from django.urls import reverse -from wireless.choices import * -from wireless.models import * from dcim.choices import InterfaceTypeChoices from dcim.models import Interface +from tenancy.models import Tenant from utilities.testing import APITestCase, APIViewTestCases, create_test_device +from wireless.choices import * +from wireless.models import * class AppTest(APITestCase): @@ -52,6 +53,12 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase): @classmethod def setUpTestData(cls): + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + ) + Tenant.objects.bulk_create(tenants) + groups = ( WirelessLANGroup(name='Group 1', slug='group-1'), WirelessLANGroup(name='Group 2', slug='group-2'), @@ -71,21 +78,25 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase): { 'ssid': 'WLAN4', 'group': groups[0].pk, + 'tenant': tenants[0].pk, 'auth_type': WirelessAuthTypeChoices.TYPE_OPEN, }, { 'ssid': 'WLAN5', 'group': groups[1].pk, + 'tenant': tenants[0].pk, 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, }, { 'ssid': 'WLAN6', + 'tenant': tenants[0].pk, 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE, }, ] cls.bulk_update_data = { 'group': groups[2].pk, + 'tenant': tenants[1].pk, 'description': 'New description', 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, 'auth_cipher': WirelessAuthCipherChoices.CIPHER_AES, @@ -115,10 +126,16 @@ class WirelessLinkTest(APIViewTestCases.APIViewTestCase): ] Interface.objects.bulk_create(interfaces) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + ) + Tenant.objects.bulk_create(tenants) + 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(ssid='LINK1', interface_a=interfaces[0], interface_b=interfaces[1], tenant=tenants[0]), + WirelessLink(ssid='LINK2', interface_a=interfaces[2], interface_b=interfaces[3], tenant=tenants[0]), + WirelessLink(ssid='LINK3', interface_a=interfaces[4], interface_b=interfaces[5], tenant=tenants[0]), ) WirelessLink.objects.bulk_create(wireless_links) @@ -127,15 +144,18 @@ class WirelessLinkTest(APIViewTestCases.APIViewTestCase): 'interface_a': interfaces[6].pk, 'interface_b': interfaces[7].pk, 'ssid': 'LINK4', + 'tenant': tenants[1].pk, }, { 'interface_a': interfaces[8].pk, 'interface_b': interfaces[9].pk, 'ssid': 'LINK5', + 'tenant': tenants[1].pk, }, { 'interface_a': interfaces[10].pk, 'interface_b': interfaces[11].pk, 'ssid': 'LINK6', + 'tenant': tenants[1].pk, }, ] diff --git a/netbox/wireless/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py index 5fee4fbf4..ffe919c32 100644 --- a/netbox/wireless/tests/test_filtersets.py +++ b/netbox/wireless/tests/test_filtersets.py @@ -3,6 +3,7 @@ from django.test import TestCase from dcim.choices import InterfaceTypeChoices, LinkStatusChoices from dcim.models import Interface from ipam.models import VLAN +from tenancy.models import Tenant from wireless.choices import * from wireless.filtersets import * from wireless.models import * @@ -43,10 +44,6 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): 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]} @@ -81,10 +78,17 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): ) VLAN.objects.bulk_create(vlans) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + 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(ssid='WLAN1', group=groups[0], vlan=vlans[0], tenant=tenants[0], auth_type=WirelessAuthTypeChoices.TYPE_OPEN, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_psk='PSK1'), + WirelessLAN(ssid='WLAN2', group=groups[1], vlan=vlans[1], tenant=tenants[1], auth_type=WirelessAuthTypeChoices.TYPE_WEP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_psk='PSK2'), + WirelessLAN(ssid='WLAN3', group=groups[2], vlan=vlans[2], tenant=tenants[2], auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, auth_psk='PSK3'), ) WirelessLAN.objects.bulk_create(wireless_lans) @@ -116,6 +120,13 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'auth_psk': ['PSK1', 'PSK2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = WirelessLink.objects.all() @@ -124,6 +135,13 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( create_test_device('device1'), create_test_device('device2'), @@ -152,6 +170,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): auth_type=WirelessAuthTypeChoices.TYPE_OPEN, auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO, auth_psk='PSK1', + tenant=tenants[0], description='foobar1' ).save() WirelessLink( @@ -162,6 +181,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): auth_type=WirelessAuthTypeChoices.TYPE_WEP, auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP, auth_psk='PSK2', + tenant=tenants[1], description='foobar2' ).save() WirelessLink( @@ -171,7 +191,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): status=LinkStatusChoices.STATUS_DECOMMISSIONING, auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL, auth_cipher=WirelessAuthCipherChoices.CIPHER_AES, - auth_psk='PSK3' + auth_psk='PSK3', + tenant=tenants[2], ).save() WirelessLink( interface_a=interfaces[5], @@ -202,3 +223,10 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): def test_description(self): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + 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 index 4141af6d6..7dea17d15 100644 --- a/netbox/wireless/tests/test_views.py +++ b/netbox/wireless/tests/test_views.py @@ -2,6 +2,7 @@ from wireless.choices import * from wireless.models import * from dcim.choices import InterfaceTypeChoices, LinkStatusChoices from dcim.models import Interface +from tenancy.models import Tenant from utilities.testing import ViewTestCases, create_tags, create_test_device @@ -47,6 +48,13 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + groups = ( WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'), WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'), @@ -55,9 +63,9 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): group.save() WirelessLAN.objects.bulk_create([ - WirelessLAN(group=groups[0], ssid='WLAN1'), - WirelessLAN(group=groups[0], ssid='WLAN2'), - WirelessLAN(group=groups[0], ssid='WLAN3'), + WirelessLAN(group=groups[0], ssid='WLAN1', tenant=tenants[0]), + WirelessLAN(group=groups[0], ssid='WLAN2', tenant=tenants[0]), + WirelessLAN(group=groups[0], ssid='WLAN3', tenant=tenants[0]), ]) tags = create_tags('Alpha', 'Bravo', 'Charlie') @@ -65,14 +73,15 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): cls.form_data = { 'ssid': 'WLAN2', 'group': groups[1].pk, + 'tenant': tenants[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", + f"group,ssid,tenant", + f"Wireless LAN Group 2,WLAN4,{tenants[0].name}", + f"Wireless LAN Group 2,WLAN5,{tenants[1].name}", + f"Wireless LAN Group 2,WLAN6,{tenants[2].name}", ) cls.bulk_edit_data = { @@ -85,6 +94,14 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): + + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + device = create_test_device('test-device') interfaces = [ Interface( @@ -98,9 +115,9 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): ] 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() + WirelessLink(interface_a=interfaces[0], interface_b=interfaces[1], ssid='LINK1', tenant=tenants[0]).save() + WirelessLink(interface_a=interfaces[2], interface_b=interfaces[3], ssid='LINK2', tenant=tenants[0]).save() + WirelessLink(interface_a=interfaces[4], interface_b=interfaces[5], ssid='LINK3', tenant=tenants[0]).save() tags = create_tags('Alpha', 'Bravo', 'Charlie') @@ -108,14 +125,15 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'interface_a': interfaces[6].pk, 'interface_b': interfaces[7].pk, 'status': LinkStatusChoices.STATUS_PLANNED, + 'tenant': tenants[1].pk, '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", + f"interface_a,interface_b,status,tenant", + f"{interfaces[6].pk},{interfaces[7].pk},connected,{tenants[0].name}", + f"{interfaces[8].pk},{interfaces[9].pk},connected,{tenants[1].name}", + f"{interfaces[10].pk},{interfaces[11].pk},connected,{tenants[2].name}", ) cls.bulk_edit_data = {