1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Introduce the wireless app and SSID model

This commit is contained in:
jeremystretch
2021-10-12 12:27:12 -04:00
parent 8e1535f7ec
commit 8b80b0c3df
28 changed files with 470 additions and 0 deletions

View File

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

View File

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

View File

@ -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( IPAM_MENU = Menu(
label='IPAM', label='IPAM',
icon_class='mdi mdi-counter', icon_class='mdi mdi-counter',
@ -343,6 +356,7 @@ MENUS = [
ORGANIZATION_MENU, ORGANIZATION_MENU,
DEVICES_MENU, DEVICES_MENU,
CONNECTIONS_MENU, CONNECTIONS_MENU,
WIRELESS_MENU,
IPAM_MENU, IPAM_MENU,
VIRTUALIZATION_MENU, VIRTUALIZATION_MENU,
CIRCUITS_MENU, CIRCUITS_MENU,

View File

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

View File

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

View File

@ -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 %}

View File

View File

View File

@ -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']

View File

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

View File

@ -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

View File

@ -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
netbox/wireless/apps.py Normal file
View File

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

View File

@ -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)

View File

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

View File

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

View File

@ -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')

View File

@ -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)

View File

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

View File

View File

@ -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)

View File

@ -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

View File

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

View File

40
netbox/wireless/models.py Normal file
View File

@ -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
netbox/wireless/tables.py Normal file
View File

@ -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
netbox/wireless/urls.py Normal file
View File

@ -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
netbox/wireless/views.py Normal file
View File

@ -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