Skip to content

Commit

Permalink
[feature] Added device groups #203
Browse files Browse the repository at this point in the history
A group can be specified for devices, i.e. DeviceGroup.
DeviceGroup contains metadata about group of deivces in form of JSON.
JSONSchema can be set systemwide for validation of entered metadata.

Added REST API endpoint for listing, creating and retrieving DeviceGroups.

Closes #203
  • Loading branch information
pandafy committed Jun 15, 2021
1 parent 1d0ccca commit 7e03a07
Show file tree
Hide file tree
Showing 23 changed files with 641 additions and 20 deletions.
62 changes: 60 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,18 @@ Allows to specify backend URL for API requests, if the frontend is hosted separa
+--------------+----------+

Allows to specify a `list` of tuples for adding commands as described in
`'How to add commands" <#how-to-add-commands>`_ section.
`'How to add commands" <#how-to-add-commands>`_ section.

``OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+--------------+----------+
| **type**: | ``dict`` |
+--------------+----------+
| **default**: | ``{}`` |
+--------------+----------+

Allows specifying JSONSchema used for validating meta-data of `Device Group <#device-group>`_.

REST API
--------
Expand Down Expand Up @@ -802,6 +813,27 @@ List locations with devices deployed (in GeoJSON format)
GET api/v1/controller/location/geojson/
List device groups
^^^^^^^^^^^^^^^^^^

.. code:: text
GET api/v1/controller/group/
Create device group
^^^^^^^^^^^^^^^^^^^

.. code:: text
POST api/v1/controller/group/
Get device group detail
^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: text
GET /api/v1/controller/group/{id}/
List templates
^^^^^^^^^^^^^^

Expand Down Expand Up @@ -1627,6 +1659,19 @@ The signal is emitted when the device name changes.

It is not emitted when the device is created.

``device_group_changed``
~~~~~~~~~~~~~~~~~~~~~~~~

**Path**: ``openwisp_controller.config.signals.device_group_changed``

**Arguments**:

- ``instance``: instance of ``Device``.

The signal is emitted when the device group changes.

It is not emitted when the device is created.

Setup (integrate in an existing django project)
-----------------------------------------------

Expand Down Expand Up @@ -2008,6 +2053,7 @@ Once you have created the models, add the following to your ``settings.py``:
# Setting models for swapper module
CONFIG_DEVICE_MODEL = 'sample_config.Device'
CONFIG_DEVICEGROUP_MODEL = 'sample_config.DeviceGroup'
CONFIG_CONFIG_MODEL = 'sample_config.Config'
CONFIG_TEMPLATETAG_MODEL = 'sample_config.TemplateTag'
CONFIG_TAGGEDTEMPLATE_MODEL = 'sample_config.TaggedTemplate'
Expand Down Expand Up @@ -2077,7 +2123,12 @@ sample_config

.. code-block:: python
from openwisp_controller.config.admin import DeviceAdmin, TemplateAdmin, VpnAdmin
from openwisp_controller.config.admin import (
DeviceAdmin,
DeviceGroupAdmin,
TemplateAdmin,
VpnAdmin,
)
# DeviceAdmin.fields += ['example'] <-- monkey patching example
Expand Down Expand Up @@ -2124,14 +2175,17 @@ sample_config
DeviceAdmin as BaseDeviceAdmin,
TemplateAdmin as BaseTemplateAdmin,
VpnAdmin as BaseVpnAdmin,
DeviceGroupAdmin as BaseDeviceGroupAdmin,
from swapper import load_model
Vpn = load_model('openwisp_controller', 'Vpn')
Device = load_model('openwisp_controller', 'Device')
DeviceGroup = load_model('openwisp_controller', 'DeviceGroup')
Template = load_model('openwisp_controller', 'Template')
admin.site.unregister(Vpn)
admin.site.unregister(Device)
admin.site.unregister(DeviceGroup)
admin.site.unregister(Template)
@admin.register(Vpn)
Expand All @@ -2142,6 +2196,10 @@ sample_config
class DeviceAdmin(BaseDeviceAdmin):
# add your changes here
@admin.register(DeviceGroup)
class DeviceGroupAdmin(BaseDeviceGroupAdmin):
# add your changes here
@admin.register(Template)
class TemplateAdmin(BaseTemplateAdmin):
# add your changes here
Expand Down
66 changes: 62 additions & 4 deletions openwisp_controller/config/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
ObjectDoesNotExist,
ValidationError,
)
from django.http import Http404, HttpResponse
from django.http import Http404, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.template.response import TemplateResponse
from django.urls import reverse
from django.urls import path, reverse
from django.utils.translation import ugettext_lazy as _
from flat_json_widget.widgets import FlatJsonWidget
from swapper import load_model
Expand All @@ -41,12 +41,13 @@
from . import settings as app_settings
from .base.vpn import AbstractVpn
from .utils import send_file
from .widgets import JsonSchemaWidget
from .widgets import DeviceGroupJsonSchemaWidget, JsonSchemaWidget

logger = logging.getLogger(__name__)
prefix = 'config/'
Config = load_model('config', 'Config')
Device = load_model('config', 'Device')
DeviceGroup = load_model('config', 'DeviceGroup')
Template = load_model('config', 'Template')
Vpn = load_model('config', 'Vpn')
Organization = load_model('openwisp_users', 'Organization')
Expand Down Expand Up @@ -385,6 +386,7 @@ class DeviceAdmin(MultitenantAdminMixin, BaseConfigAdmin, UUIDAdmin):
list_display = [
'name',
'backend',
'group',
'config_status',
'mac_address',
'ip',
Expand All @@ -397,14 +399,24 @@ class DeviceAdmin(MultitenantAdminMixin, BaseConfigAdmin, UUIDAdmin):
'config__status',
'created',
]
search_fields = ['id', 'name', 'mac_address', 'key', 'model', 'os', 'system']
search_fields = [
'id',
'name',
'mac_address',
'key',
'model',
'os',
'system',
]
readonly_fields = ['last_ip', 'management_ip', 'uuid']
autocomplete_fields = ['group']
fields = [
'name',
'organization',
'mac_address',
'uuid',
'key',
'group',
'last_ip',
'management_ip',
'model',
Expand Down Expand Up @@ -698,9 +710,55 @@ class Media(BaseConfigAdmin):
js = list(BaseConfigAdmin.Media.js) + [f'{prefix}js/vpn.js']


class DeviceGroupForm(BaseForm):
class Meta(BaseForm.Meta):
model = DeviceGroup
widgets = {'context': DeviceGroupJsonSchemaWidget}
labels = {'context': _('Metadata')}
help_texts = {
'context': _(
# TODO: Rephrase this
'In this section you can add metadata of the DeviceGroup'
)
}


class DeviceGroupAdmin(BaseAdmin):
form = DeviceGroupForm
fields = [
'name',
'organization',
'description',
'context',
'created',
'modified',
]
search_fields = ['name']

class Media:
css = {'all': (f'{prefix}css/admin.css',)}

def get_urls(self):
options = self.model._meta
url_prefix = f'{options.app_label}_{options.model_name}'
urls = super().get_urls()
urls += [
path(
f'{options.app_label}/{options.model_name}/ui/schema.json',
self.admin_site.admin_view(self.schema_view),
name=f'{url_prefix}_schema',
),
]
return urls

def schema_view(self, request):
return JsonResponse(app_settings.DEVICE_GROUP_SCHEMA)


admin.site.register(Device, DeviceAdmin)
admin.site.register(Template, TemplateAdmin)
admin.site.register(Vpn, VpnAdmin)
admin.site.register(DeviceGroup, DeviceGroupAdmin)


if getattr(app_settings, 'REGISTRATION_ENABLED', True):
Expand Down
11 changes: 11 additions & 0 deletions openwisp_controller/config/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Template = load_model('config', 'Template')
Vpn = load_model('config', 'Vpn')
Device = load_model('config', 'Device')
DeviceGroup = load_model('config', 'DeviceGroup')
Config = load_model('config', 'Config')
Organization = load_model('openwisp_users', 'Organization')

Expand Down Expand Up @@ -132,6 +133,7 @@ class Meta(BaseMeta):
'id',
'name',
'organization',
'group',
'mac_address',
'key',
'last_ip',
Expand Down Expand Up @@ -184,6 +186,7 @@ class Meta(BaseMeta):
'id',
'name',
'organization',
'group',
'mac_address',
'key',
'last_ip',
Expand Down Expand Up @@ -256,3 +259,11 @@ def update(self, instance, validated_data):
instance.config.full_clean()
instance.config.save()
return super().update(instance, validated_data)


class DeviceGroupSerializer(BaseSerializer):
context = serializers.JSONField(required=False, initial={})

class Meta(BaseMeta):
model = DeviceGroup
fields = ['name', 'organization', 'description', 'context']
10 changes: 10 additions & 0 deletions openwisp_controller/config/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ def get_api_urls(api_views):
api_views.device_detail,
name='device_detail',
),
path(
'controller/group/',
api_views.devicegroup_list,
name='devicegroup_list',
),
path(
'controller/group/<str:pk>/',
api_views.devicegroup_detail,
name='devicegroup_detail',
),
path(
'controller/device/<str:pk>/configuration/',
api_views.download_device_config,
Expand Down
21 changes: 19 additions & 2 deletions openwisp_controller/config/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ..admin import BaseConfigAdmin
from .serializers import (
DeviceDetailSerializer,
DeviceGroupSerializer,
DeviceListSerializer,
TemplateSerializer,
VpnSerializer,
Expand All @@ -23,6 +24,7 @@
Template = load_model('config', 'Template')
Vpn = load_model('config', 'Vpn')
Device = load_model('config', 'Device')
DeviceGroup = load_model('config', 'DeviceGroup')
Config = load_model('config', 'Config')


Expand Down Expand Up @@ -87,7 +89,9 @@ class DeviceListCreateView(ProtectedAPIMixin, ListCreateAPIView):
"""

serializer_class = DeviceListSerializer
queryset = Device.objects.select_related('config').order_by('-created')
queryset = Device.objects.select_related(
'config', 'group', 'organization'
).order_by('-created')
pagination_class = ListViewPagination


Expand All @@ -98,7 +102,7 @@ class DeviceDetailView(ProtectedAPIMixin, RetrieveUpdateDestroyAPIView):
"""

serializer_class = DeviceDetailSerializer
queryset = Device.objects.select_related('config')
queryset = Device.objects.select_related('config', 'group', 'organization')


class DownloadDeviceView(ProtectedAPIMixin, RetrieveAPIView):
Expand All @@ -110,6 +114,17 @@ def retrieve(self, request, *args, **kwargs):
return BaseConfigAdmin.download_view(self, request, pk=kwargs['pk'])


class DeviceGroupListCreateView(ProtectedAPIMixin, ListCreateAPIView):
serializer_class = DeviceGroupSerializer
queryset = DeviceGroup.objects.select_related('organization').order_by('-created')
pagination_class = ListViewPagination


class DeviceGroupDetailView(ProtectedAPIMixin, ListCreateAPIView):
serializer_class = DeviceGroupSerializer
queryset = DeviceGroup.objects.select_related('organization').order_by('-created')


template_list = TemplateListCreateView.as_view()
template_detail = TemplateDetailView.as_view()
download_template_config = DownloadTemplateconfiguration.as_view()
Expand All @@ -118,4 +133,6 @@ def retrieve(self, request, *args, **kwargs):
download_vpn_config = DownloadVpnView.as_view()
device_list = DeviceListCreateView.as_view()
device_detail = DeviceDetailView.as_view()
devicegroup_list = DeviceGroupListCreateView.as_view()
devicegroup_detail = DeviceGroupDetailView.as_view()
download_device_config = DownloadDeviceView().as_view()
Loading

0 comments on commit 7e03a07

Please sign in to comment.