Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Added device groups #203 #492

Merged
merged 15 commits into from
Jul 15, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_DEVICEGROUP_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.
pandafy marked this conversation as resolved.
Show resolved Hide resolved
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_DEVICEGROUP_SCHEMA" <#openwisp-controller-devicegroup-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.

``devicegroup_changed``
~~~~~~~~~~~~~~~~~~~~~~~

**Path**: ``openwisp_controller.config.signals.devicegroup_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.
pandafy marked this conversation as resolved.
Show resolved Hide resolved

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']
pandafy marked this conversation as resolved.
Show resolved Hide resolved
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.DEVICEGROUP_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