jeremystretch 4 лет назад
Родитель
Сommit
90e9f34494

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

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

+ 1 - 0
netbox/netbox/navigation_menu.py

@@ -196,6 +196,7 @@ WIRELESS_MENU = Menu(
             label='Wireless',
             items=(
                 get_model_item('wireless', 'wirelesslan', 'Wireless LANs'),
+                get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']),
             ),
         ),
     ),

+ 20 - 0
netbox/templates/wireless/inc/wirelesslink_interface.html

@@ -0,0 +1,20 @@
+<table class="table table-hover panel-body attr-table">
+  <tr>
+    <td>Device</td>
+    <td>
+      <a href="{{ interface.device.get_absolute_url }}">{{ interface.device }}</a>
+    </td>
+  </tr>
+  <tr>
+    <td>Interface</td>
+    <td>
+      <a href="{{ interface.get_absolute_url }}">{{ interface }}</a>
+    </td>
+  </tr>
+  <tr>
+    <td>Type</td>
+    <td>
+      {{ interface.get_type_display }}
+    </td>
+  </tr>
+</table>

+ 1 - 1
netbox/templates/wireless/wirelesslan.html

@@ -30,7 +30,7 @@
                 </table>
             </div>
         </div>
-        {% 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 %}
     </div>
     <div class="col col-md-6">

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

@@ -0,0 +1,48 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block content %}
+  <div class="row">
+    <div class="col col-md-6">
+      <div class="card">
+        <h5 class="card-header">Interface A</h5>
+        <div class="card-body">
+          {% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_a %}
+        </div>
+      </div>
+      <div class="card">
+        <h5 class="card-header">Link Properties</h5>
+        <div class="card-body">
+          <table class="table table-hover attr-table">
+            <tr>
+              <th scope="row">SSID</th>
+              <td>{{ object.ssid|placeholder }}</td>
+            </tr>
+            <tr>
+              <th scope="row">Description</th>
+              <td>{{ object.description|placeholder }}</td>
+            </tr>
+          </table>
+        </div>
+      </div>
+      {% plugin_left_page object %}
+    </div>
+    <div class="col col-md-6">
+      <div class="card">
+        <h5 class="card-header">Interface B</h5>
+        <div class="card-body">
+          {% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_b %}
+        </div>
+      </div>
+      {% 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 %}
+    </div>
+  </div>
+  <div class="row">
+    <div class="col col-md-12">
+      {% plugin_full_width_page object %}
+    </div>
+  </div>
+{% endblock %}

+ 10 - 1
netbox/wireless/api/nested_serializers.py

@@ -5,12 +5,21 @@ from wireless.models import *
 
 __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']

+ 16 - 1
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 WirelessLANSerializer(PrimaryModelSerializer):
         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',
+        ]

+ 1 - 0
netbox/wireless/api/urls.py

@@ -6,6 +6,7 @@ router = OrderedDefaultRouter()
 router.APIRootView = views.WirelessRootView
 
 router.register('wireless-lans', views.WirelessLANViewSet)
+router.register('wireless-links', views.WirelessLinkViewSet)
 
 app_name = 'wireless-api'
 urlpatterns = router.urls

+ 7 - 5
netbox/wireless/api/views.py

@@ -14,11 +14,13 @@ class WirelessRootView(APIRootView):
         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

+ 22 - 0
netbox/wireless/filtersets.py

@@ -7,6 +7,7 @@ from .models import *
 
 __all__ = (
     'WirelessLANFilterSet',
+    'WirelessLinkFilterSet',
 )
 
 
@@ -29,3 +30,24 @@ class WirelessLANFilterSet(PrimaryModelFilterSet):
             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)

+ 24 - 3
netbox/wireless/forms/bulk_edit.py

@@ -4,9 +4,11 @@ from dcim.models import *
 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']

+ 16 - 1
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')

+ 27 - 1
netbox/wireless/forms/filtersets.py

@@ -3,7 +3,12 @@ from django.utils.translation import gettext as _
 
 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)

+ 28 - 1
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 WirelessLANForm(BootstrapMixin, CustomFieldModelForm):
             ('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',
+        ]

+ 0 - 34
netbox/wireless/migrations/0001_initial.py

@@ -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'),
-            },
-        ),
-    ]

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

+ 80 - 3
netbox/wireless/models.py

@@ -6,19 +6,22 @@ from dcim.constants import WIRELESS_IFACE_TYPES
 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 @@ class WirelessLAN(PrimaryModel):
         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)

+ 24 - 1
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 WirelessLANTable(BaseTable):
         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')

+ 12 - 0
netbox/wireless/urls.py

@@ -19,4 +19,16 @@ urlpatterns = (
     path('wireless-lans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslan_changelog', kwargs={'model': WirelessLAN}),
     path('wireless-lans/<int:pk>/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/<int:pk>/', views.WirelessLinkView.as_view(), name='wirelesslink'),
+    path('wireless-links/<int:pk>/edit/', views.WirelessLinkEditView.as_view(), name='wirelesslink_edit'),
+    path('wireless-links/<int:pk>/delete/', views.WirelessLinkDeleteView.as_view(), name='wirelesslink_delete'),
+    path('wireless-links/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslink_changelog', kwargs={'model': WirelessLink}),
+    path('wireless-links/<int:pk>/journal/', ObjectJournalView.as_view(), name='wirelesslink_journal', kwargs={'model': WirelessLink}),
+
 )

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