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 can contain metadata about group of devices in JSON format.
JSONSchema can be set systemwide for validating and building the UI users will use to fill the metadata.

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

Implements and closes #203
  • Loading branch information
pandafy authored Jul 15, 2021
1 parent 030fc9d commit 734a6c8
Show file tree
Hide file tree
Showing 28 changed files with 823 additions and 45 deletions.
80 changes: 78 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Config App
* **configuration templates**: reduce repetition to the minimum
* `configuration variables <#how-to-use-configuration-variables>`_: reference ansible-like variables in the configuration and templates
* **template tags**: tag templates to automate different types of auto-configurations (eg: mesh, WDS, 4G)
* **device groups**: add `devices to dedicated groups <#device-groups>`_ for easy management
* **simple HTTP resources**: allow devices to automatically download configuration updates
* **VPN management**: automatically provision VPN tunnels with unique x509 certificates

Expand Down Expand Up @@ -625,7 +626,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**: | ``{'type': 'object', 'properties': {}}`` |
+--------------+------------------------------------------+

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

REST API
--------
Expand Down Expand Up @@ -807,6 +819,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 @@ -1050,6 +1083,21 @@ Please refer
`troubleshooting issues related to geospatial libraries
<https://docs.djangoproject.com/en/2.1/ref/contrib/gis/install/#library-environment-settings/>`_.

Device Groups
-------------

Device Groups provide an easy way to organize devices of a particular organization.
You can achieve following by using Device Groups:

- Group similar devices by having dedicated groups for access points, routers, etc.
- Store additional information regarding a group in the structured metadata field.
- Customize structure and validation of metadata field of DeviceGroup to standardize
information across all groups using `"OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA" <#openwisp-controller-device-group-schema>`_
setting.

.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/master/docs/device-groups.png
:alt: Device Group example

How to use configuration variables
----------------------------------

Expand Down Expand Up @@ -1632,6 +1680,21 @@ 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``.
- ``group_id``: primary key of ``DeviceGroup`` of ``Device``
- ``old_group_id``: primary key of previous ``DeviceGroup`` 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 @@ -2013,6 +2076,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 @@ -2082,7 +2146,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 @@ -2129,14 +2198,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 @@ -2147,6 +2219,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
Binary file added docs/device-groups.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 56 additions & 3 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 @@ -408,12 +410,14 @@ class DeviceAdmin(MultitenantAdminMixin, BaseConfigAdmin, UUIDAdmin):
'devicelocation__location__address',
]
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 @@ -707,9 +711,58 @@ class Media(BaseConfigAdmin):
js = list(BaseConfigAdmin.Media.js) + [f'{prefix}js/vpn.js']


class DeviceGroupForm(BaseForm):
class Meta(BaseForm.Meta):
model = DeviceGroup
widgets = {'meta_data': DeviceGroupJsonSchemaWidget}
labels = {'meta_data': _('Metadata')}
help_texts = {
'meta_data': _(
'Group meta data, use this field to store data which is related'
'to this group and can be retrieved via the REST API.'
)
}


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

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
19 changes: 19 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,19 @@ def update(self, instance, validated_data):
instance.config.full_clean()
instance.config.save()
return super().update(instance, validated_data)


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

class Meta(BaseMeta):
model = DeviceGroup
fields = [
'id',
'name',
'organization',
'description',
'meta_data',
'created',
'modified',
]
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, RetrieveUpdateDestroyAPIView):
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()
1 change: 1 addition & 0 deletions openwisp_controller/config/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def add_default_menu_items(self):
{'model': get_model_name('config', 'Device')},
{'model': get_model_name('config', 'Template')},
{'model': get_model_name('config', 'Vpn')},
{'model': get_model_name('config', 'DeviceGroup')},
]
if not hasattr(settings, menu_setting):
setattr(settings, menu_setting, items)
Expand Down
Loading

0 comments on commit 734a6c8

Please sign in to comment.