Просмотр исходного кода

Closes #9177: Add tenant assignment for wireless LANs & links

jeremystretch 3 лет назад
Родитель
Сommit
7dd5f9e720

+ 1 - 1
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.
 

+ 1 - 1
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:
 

+ 5 - 0
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

+ 10 - 2
netbox/templates/tenancy/tenant.html

@@ -61,6 +61,10 @@
                     <h2><a href="{% url 'dcim:device_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.device_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
                     <p>Devices</p>
                 </div>
+                <div class="col col-md-4 text-center">
+                    <h2><a href="{% url 'dcim:cable_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.cable_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.cable_count }}</a></h2>
+                    <p>Cables</p>
+                </div>
                 <div class="col col-md-4 text-center">
                     <h2><a href="{% url 'ipam:vrf_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vrf_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
                     <p>VRFs</p>
@@ -102,8 +106,12 @@
                     <p>Clusters</p>
                 </div>
                 <div class="col col-md-4 text-center">
-                    <h2><a href="{% url 'dcim:cable_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.cable_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.cable_count }}</a></h2>
-                    <p>Cables</p>
+                    <h2><a href="{% url 'wireless:wirelesslan_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.wirelesslan_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.wirelesslan_count }}</a></h2>
+                    <p>Wireless LANs</p>
+                </div>
+                <div class="col col-md-4 text-center">
+                    <h2><a href="{% url 'wireless:wirelesslink_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.wirelesslink_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.wirelesslink_count }}</a></h2>
+                    <p>Wireless Links</p>
                 </div>
             </div>
         </div>

+ 38 - 29
netbox/templates/wireless/wirelesslan.html

@@ -6,36 +6,45 @@
 {% block content %}
 <div class="row">
 	<div class="col col-md-6">
-        <div class="card">
-            <h5 class="card-header">Wireless LAN</h5>
-            <div class="card-body">
-                <table class="table table-hover attr-table">
-                    <tr>
-                        <th scope="row">SSID</th>
-                        <td>{{ object.ssid }}</td>
-                    </tr>
-                    <tr>
-                        <td>Group</td>
-                        <td>{{ object.group|linkify|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Description</th>
-                        <td>{{ object.description|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">VLAN</th>
-                        <td>{{ object.vlan|linkify|placeholder }}</td>
-                    </tr>
-                </table>
-            </div>
-        </div>
-        {% include 'inc/panels/tags.html' %}
-        {% plugin_left_page object %}
+    <div class="card">
+      <h5 class="card-header">Wireless LAN</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">SSID</th>
+            <td>{{ object.ssid }}</td>
+          </tr>
+          <tr>
+            <td>Group</td>
+            <td>{{ object.group|linkify|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Description</th>
+            <td>{{ object.description|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">VLAN</th>
+            <td>{{ object.vlan|linkify|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Tenant</th>
+            <td>
+              {% if object.tenant.group %}
+                {{ object.tenant.group|linkify }} /
+              {% endif %}
+              {{ object.tenant|linkify|placeholder }}
+            </td>
+          </tr>
+        </table>
+      </div>
     </div>
-    <div class="col col-md-6">
-        {% 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 %}
+  </div>
+  <div class="col col-md-6">
+    {% include 'wireless/inc/authentication_attrs.html' %}
+    {% include 'inc/panels/custom_fields.html' %}
+    {% plugin_right_page object %}
 	</div>
 </div>
 <div class="row">

+ 9 - 0
netbox/templates/wireless/wirelesslink.html

@@ -23,6 +23,15 @@
               <th scope="row">SSID</th>
               <td>{{ object.ssid|placeholder }}</td>
             </tr>
+            <tr>
+              <th scope="row">Tenant</th>
+              <td>
+                {% if object.tenant.group %}
+                  {{ object.tenant.group|linkify }} /
+                {% endif %}
+                {{ object.tenant|linkify|placeholder }}
+              </td>
+            </tr>
             <tr>
               <th scope="row">Description</th>
               <td>{{ object.description|placeholder }}</td>

+ 3 - 0
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 {

+ 6 - 3
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',
         ]

+ 2 - 2
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

+ 3 - 2
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(

+ 13 - 4
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',
     )

+ 17 - 2
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',
+        )

+ 10 - 2
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'

+ 8 - 5
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,

+ 25 - 0
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'),
+        ),
+    ]

+ 14 - 0
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

+ 4 - 2
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')
 

+ 3 - 1
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 = (

+ 25 - 5
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,
             },
         ]

+ 36 - 8
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)

+ 32 - 14
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 = {