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

Introduce the wireless app and SSID model

jeremystretch 4 лет назад
Родитель
Сommit
8b80b0c3df

+ 1 - 0
netbox/netbox/api/views.py

@@ -308,6 +308,7 @@ class APIRootView(APIView):
             ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
             ('users', reverse('users-api:api-root', request=request, format=format)),
             ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
+            ('wireless', reverse('wireless-api:api-root', request=request, format=format)),
         )))
 
 

+ 2 - 0
netbox/netbox/graphql/schema.py

@@ -7,6 +7,7 @@ from ipam.graphql.schema import IPAMQuery
 from tenancy.graphql.schema import TenancyQuery
 from users.graphql.schema import UsersQuery
 from virtualization.graphql.schema import VirtualizationQuery
+from wireless.graphql.schema import WirelessQuery
 
 
 class Query(
@@ -17,6 +18,7 @@ class Query(
     TenancyQuery,
     UsersQuery,
     VirtualizationQuery,
+    WirelessQuery,
     graphene.ObjectType
 ):
     pass

+ 14 - 0
netbox/netbox/navigation_menu.py

@@ -188,6 +188,19 @@ CONNECTIONS_MENU = Menu(
     ),
 )
 
+WIRELESS_MENU = Menu(
+    label='Wireless',
+    icon_class='mdi mdi-wifi',
+    groups=(
+        MenuGroup(
+            label='Wireless',
+            items=(
+                get_model_item('wireless', 'ssid', 'SSIDs'),
+            ),
+        ),
+    ),
+)
+
 IPAM_MENU = Menu(
     label='IPAM',
     icon_class='mdi mdi-counter',
@@ -343,6 +356,7 @@ MENUS = [
     ORGANIZATION_MENU,
     DEVICES_MENU,
     CONNECTIONS_MENU,
+    WIRELESS_MENU,
     IPAM_MENU,
     VIRTUALIZATION_MENU,
     CIRCUITS_MENU,

+ 1 - 0
netbox/netbox/settings.py

@@ -326,6 +326,7 @@ INSTALLED_APPS = [
     'users',
     'utilities',
     'virtualization',
+    'wireless',
     'django_rq',  # Must come after extras to allow overriding management commands
     'drf_yasg',
 ]

+ 2 - 0
netbox/netbox/urls.py

@@ -48,6 +48,7 @@ _patterns = [
     path('tenancy/', include('tenancy.urls')),
     path('user/', include('users.urls')),
     path('virtualization/', include('virtualization.urls')),
+    path('wireless/', include('wireless.urls')),
 
     # API
     path('api/', APIRootView.as_view(), name='api-root'),
@@ -58,6 +59,7 @@ _patterns = [
     path('api/tenancy/', include('tenancy.api.urls')),
     path('api/users/', include('users.api.urls')),
     path('api/virtualization/', include('virtualization.api.urls')),
+    path('api/wireless/', include('wireless.api.urls')),
     path('api/status/', StatusView.as_view(), name='api-status'),
     path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'),
     path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'),

+ 46 - 0
netbox/templates/wireless/ssid.html

@@ -0,0 +1,46 @@
+{% 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">SSID</h5>
+            <div class="card-body">
+                <table class="table table-hover attr-table">
+                    <tr>
+                        <th scope="row">Name</th>
+                        <td>{{ object.name }}</td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Description</th>
+                        <td>{{ object.description|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <th scope="row">VLAN</th>
+                        <td>
+                            {% if object.vlan %}
+                                <a href="{{ object.vlan.get_absolute_url }}">{{ object.vlan }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                </table>
+            </div>
+        </div>
+        {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %}
+        {% plugin_left_page object %}
+    </div>
+    <div class="col col-md-6">
+        {% include 'inc/custom_fields_panel.html' %}
+        {% plugin_right_page object %}
+	</div>
+</div>
+<div class="row">
+    <div class="col col-md-12">
+        {% plugin_full_width_page object %}
+    </div>
+</div>
+{% endblock %}

+ 0 - 0
netbox/wireless/__init__.py


+ 0 - 0
netbox/wireless/api/__init__.py


+ 16 - 0
netbox/wireless/api/nested_serializers.py

@@ -0,0 +1,16 @@
+from rest_framework import serializers
+
+from netbox.api import WritableNestedSerializer
+from wireless.models import *
+
+__all__ = (
+    'NestedSSIDSerializer',
+)
+
+
+class NestedSSIDSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail')
+
+    class Meta:
+        model = SSID
+        fields = ['id', 'url', 'display', 'name']

+ 21 - 0
netbox/wireless/api/serializers.py

@@ -0,0 +1,21 @@
+from rest_framework import serializers
+
+from dcim.api.serializers import NestedInterfaceSerializer
+from ipam.api.serializers import NestedVLANSerializer
+from netbox.api.serializers import PrimaryModelSerializer
+from wireless.models import *
+
+__all__ = (
+    'SSIDSerializer',
+)
+
+
+class SSIDSerializer(PrimaryModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:ssid-detail')
+    vlan = NestedVLANSerializer(required=False, allow_null=True)
+
+    class Meta:
+        model = SSID
+        fields = [
+            'id', 'url', 'display', 'name', 'description', 'vlan',
+        ]

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

@@ -0,0 +1,12 @@
+from netbox.api import OrderedDefaultRouter
+from . import views
+
+
+router = OrderedDefaultRouter()
+router.APIRootView = views.WirelessRootView
+
+# SSIDs
+router.register('ssids', views.SSIDViewSet)
+
+app_name = 'wireless-api'
+urlpatterns = router.urls

+ 24 - 0
netbox/wireless/api/views.py

@@ -0,0 +1,24 @@
+from rest_framework.routers import APIRootView
+
+from extras.api.views import CustomFieldModelViewSet
+from wireless import filtersets
+from wireless.models import *
+from . import serializers
+
+
+class WirelessRootView(APIRootView):
+    """
+    Wireless API root view
+    """
+    def get_view_name(self):
+        return 'Wireless'
+
+
+#
+# Providers
+#
+
+class SSIDViewSet(CustomFieldModelViewSet):
+    queryset = SSID.objects.prefetch_related('tags')
+    serializer_class = serializers.SSIDSerializer
+    filterset_class = filtersets.SSIDFilterSet

+ 5 - 0
netbox/wireless/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class WirelessConfig(AppConfig):
+    name = 'wireless'

+ 31 - 0
netbox/wireless/filtersets.py

@@ -0,0 +1,31 @@
+import django_filters
+from django.db.models import Q
+
+from extras.filters import TagFilter
+from netbox.filtersets import PrimaryModelFilterSet
+from .models import *
+
+__all__ = (
+    'SSIDFilterSet',
+)
+
+
+class SSIDFilterSet(PrimaryModelFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+    tag = TagFilter()
+
+    class Meta:
+        model = SSID
+        fields = ['id', 'name']
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        qs_filter = (
+            Q(name__icontains=value) |
+            Q(description__icontains=value)
+        )
+        return queryset.filter(qs_filter)

+ 4 - 0
netbox/wireless/forms/__init__.py

@@ -0,0 +1,4 @@
+from .models import *
+from .filtersets import *
+from .bulk_edit import *
+from .bulk_import import *

+ 29 - 0
netbox/wireless/forms/bulk_edit.py

@@ -0,0 +1,29 @@
+from django import forms
+
+from dcim.models import *
+from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
+from ipam.models import VLAN
+from utilities.forms import BootstrapMixin, DynamicModelChoiceField
+
+__all__ = (
+    'SSIDBulkEditForm',
+)
+
+
+class SSIDBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
+    pk = forms.ModelMultipleChoiceField(
+        queryset=PowerFeed.objects.all(),
+        widget=forms.MultipleHiddenInput
+    )
+    vlan = DynamicModelChoiceField(
+        queryset=VLAN.objects.all(),
+        required=False,
+    )
+    description = forms.CharField(
+        required=False
+    )
+
+    class Meta:
+        nullable_fields = [
+            'vlan', 'description',
+        ]

+ 20 - 0
netbox/wireless/forms/bulk_import.py

@@ -0,0 +1,20 @@
+from extras.forms import CustomFieldModelCSVForm
+from ipam.models import VLAN
+from utilities.forms import CSVModelChoiceField
+from wireless.models import SSID
+
+__all__ = (
+    'SSIDCSVForm',
+)
+
+
+class SSIDCSVForm(CustomFieldModelCSVForm):
+    vlan = CSVModelChoiceField(
+        queryset=VLAN.objects.all(),
+        to_field_name='name',
+        help_text='Bridged VLAN'
+    )
+
+    class Meta:
+        model = SSID
+        fields = ('name', 'description', 'vlan')

+ 19 - 0
netbox/wireless/forms/filtersets.py

@@ -0,0 +1,19 @@
+from django import forms
+from django.utils.translation import gettext as _
+
+from dcim.models import *
+from extras.forms import CustomFieldModelFilterForm
+from utilities.forms import BootstrapMixin, TagFilterField
+
+
+class SSIDFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
+    model = PowerFeed
+    field_groups = [
+        ['q', 'tag'],
+    ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
+    tag = TagFilterField(model)

+ 32 - 0
netbox/wireless/forms/models.py

@@ -0,0 +1,32 @@
+from dcim.constants import *
+from dcim.models import *
+from extras.forms import CustomFieldModelForm
+from extras.models import Tag
+from ipam.models import VLAN
+from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from wireless.models import SSID
+
+__all__ = (
+    'SSIDForm',
+)
+
+
+class SSIDForm(BootstrapMixin, CustomFieldModelForm):
+    vlan = DynamicModelChoiceField(
+        queryset=VLAN.objects.all(),
+        required=False
+    )
+    tags = DynamicModelMultipleChoiceField(
+        queryset=Tag.objects.all(),
+        required=False
+    )
+
+    class Meta:
+        model = SSID
+        fields = [
+            'name', 'description', 'vlan', 'tags',
+        ]
+        fieldsets = (
+            ('SSID', ('name', 'description', 'tags')),
+            ('VLAN', ('vlan',)),
+        )

+ 0 - 0
netbox/wireless/graphql/__init__.py


+ 9 - 0
netbox/wireless/graphql/schema.py

@@ -0,0 +1,9 @@
+import graphene
+
+from netbox.graphql.fields import ObjectField, ObjectListField
+from .types import *
+
+
+class WirelessQuery(graphene.ObjectType):
+    ssid = ObjectField(SSIDType)
+    ssid_list = ObjectListField(SSIDType)

+ 14 - 0
netbox/wireless/graphql/types.py

@@ -0,0 +1,14 @@
+from wireless import filtersets, models
+from netbox.graphql.types import ObjectType
+
+__all__ = (
+    'SSIDType',
+)
+
+
+class SSIDType(ObjectType):
+
+    class Meta:
+        model = models.SSID
+        fields = '__all__'
+        filterset_class = filtersets.SSIDFilterSet

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

@@ -0,0 +1,36 @@
+import django.core.serializers.json
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('dcim', '0136_wireless'),
+        ('extras', '0062_clear_secrets_changelog'),
+        ('ipam', '0050_iprange'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SSID',
+            fields=[
+                ('created', models.DateField(auto_now_add=True, null=True)),
+                ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+                ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+                ('id', models.BigAutoField(primary_key=True, serialize=False)),
+                ('name', models.CharField(max_length=32)),
+                ('description', models.CharField(blank=True, max_length=200)),
+                ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+                ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')),
+            ],
+            options={
+                'verbose_name': 'SSID',
+                'verbose_name_plural': 'SSIDs',
+                'ordering': ('name', 'pk'),
+            },
+        ),
+    ]

+ 0 - 0
netbox/wireless/migrations/__init__.py


+ 40 - 0
netbox/wireless/models.py

@@ -0,0 +1,40 @@
+from django.db import models
+
+from extras.utils import extras_features
+from netbox.models import PrimaryModel
+from utilities.querysets import RestrictedQuerySet
+
+__all__ = (
+    'SSID',
+)
+
+
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
+class SSID(PrimaryModel):
+    """
+    A service set identifier belonging to a wireless network.
+    """
+    name = models.CharField(
+        max_length=32
+    )
+    vlan = models.ForeignKey(
+        to='ipam.VLAN',
+        on_delete=models.PROTECT,
+        blank=True,
+        null=True,
+        verbose_name='VLAN'
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True
+    )
+
+    objects = RestrictedQuerySet.as_manager()
+
+    class Meta:
+        ordering = ('name', 'pk')
+        verbose_name = 'SSID'
+        verbose_name_plural = 'SSIDs'
+
+    def __str__(self):
+        return self.name

+ 24 - 0
netbox/wireless/tables.py

@@ -0,0 +1,24 @@
+import django_tables2 as tables
+
+from .models import SSID
+from utilities.tables import BaseTable, TagColumn, ToggleColumn
+
+__all__ = (
+    'SSIDTable',
+)
+
+
+class SSIDTable(BaseTable):
+    pk = ToggleColumn()
+    id = tables.Column(
+        linkify=True,
+        verbose_name='ID'
+    )
+    tags = TagColumn(
+        url_name='dcim:cable_list'
+    )
+
+    class Meta(BaseTable.Meta):
+        model = SSID
+        fields = ('pk', 'id', 'name', 'description', 'vlan')
+        default_columns = ('pk', 'name', 'description', 'vlan')

+ 22 - 0
netbox/wireless/urls.py

@@ -0,0 +1,22 @@
+from django.urls import path
+
+from extras.views import ObjectChangeLogView, ObjectJournalView
+from . import views
+from .models import *
+
+app_name = 'wireless'
+urlpatterns = (
+
+    # SSIDs
+    path('ssids/', views.SSIDListView.as_view(), name='ssid_list'),
+    path('ssids/add/', views.SSIDEditView.as_view(), name='ssid_add'),
+    path('ssids/import/', views.SSIDBulkImportView.as_view(), name='ssid_import'),
+    path('ssids/edit/', views.SSIDBulkEditView.as_view(), name='ssid_bulk_edit'),
+    path('ssids/delete/', views.SSIDBulkDeleteView.as_view(), name='ssid_bulk_delete'),
+    path('ssids/<int:pk>/', views.SSIDView.as_view(), name='ssid'),
+    path('ssids/<int:pk>/edit/', views.SSIDEditView.as_view(), name='ssid_edit'),
+    path('ssids/<int:pk>/delete/', views.SSIDDeleteView.as_view(), name='ssid_delete'),
+    path('ssids/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ssid_changelog', kwargs={'model': SSID}),
+    path('ssids/<int:pk>/journal/', ObjectJournalView.as_view(), name='ssid_journal', kwargs={'model': SSID}),
+
+)

+ 46 - 0
netbox/wireless/views.py

@@ -0,0 +1,46 @@
+from netbox.views import generic
+from . import filtersets, forms, tables
+from .models import *
+
+
+#
+# SSIDs
+#
+
+class SSIDListView(generic.ObjectListView):
+    queryset = SSID.objects.all()
+    filterset = filtersets.SSIDFilterSet
+    filterset_form = forms.SSIDFilterForm
+    table = tables.SSIDTable
+
+
+class SSIDView(generic.ObjectView):
+    queryset = SSID.objects.prefetch_related('power_panel', 'rack')
+
+
+class SSIDEditView(generic.ObjectEditView):
+    queryset = SSID.objects.all()
+    model_form = forms.SSIDForm
+
+
+class SSIDDeleteView(generic.ObjectDeleteView):
+    queryset = SSID.objects.all()
+
+
+class SSIDBulkImportView(generic.BulkImportView):
+    queryset = SSID.objects.all()
+    model_form = forms.SSIDCSVForm
+    table = tables.SSIDTable
+
+
+class SSIDBulkEditView(generic.BulkEditView):
+    queryset = SSID.objects.prefetch_related('power_panel', 'rack')
+    filterset = filtersets.SSIDFilterSet
+    table = tables.SSIDTable
+    form = forms.SSIDBulkEditForm
+
+
+class SSIDBulkDeleteView(generic.BulkDeleteView):
+    queryset = SSID.objects.prefetch_related('power_panel', 'rack')
+    filterset = filtersets.SSIDFilterSet
+    table = tables.SSIDTable