From f1d4c94fff507ed1a698b02fa70b119bb5c1db0c Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 18 May 2022 00:30:39 +0530 Subject: [PATCH] [feature] Added monitoring of WiFi Client and Sessions #360 #361 Closes #360 Closes #361 Co-authored-by: Federico Capoano --- README.rst | 84 ++++++++- openwisp_monitoring/device/admin.py | 149 ++++++++++++++++ openwisp_monitoring/device/apps.py | 78 ++++++++- openwisp_monitoring/device/base/models.py | 82 +++++++++ .../migrations/0004_wificlient_wifisession.py | 136 +++++++++++++++ .../migrations/0005_add_group_permissions.py | 18 ++ .../device/migrations/__init__.py | 37 ++++ openwisp_monitoring/device/models.py | 19 +- openwisp_monitoring/device/settings.py | 1 + openwisp_monitoring/device/tasks.py | 66 +++++++ openwisp_monitoring/device/tests/__init__.py | 52 ++++++ .../device/tests/test_admin.py | 163 +++++++++++++++++- .../device/tests/test_models.py | 144 +++++++++++++++- openwisp_monitoring/monitoring/admin.py | 3 + .../monitoring/migrations/__init__.py | 36 ++++ requirements-test.txt | 2 +- requirements.txt | 1 + .../migrations/0001_initial.py | 117 +++++++++++++ .../sample_device_monitoring/models.py | 23 ++- .../sample_device_monitoring/tests.py | 16 ++ tests/openwisp2/settings.py | 2 + 21 files changed, 1216 insertions(+), 13 deletions(-) create mode 100644 openwisp_monitoring/device/migrations/0004_wificlient_wifisession.py create mode 100644 openwisp_monitoring/device/migrations/0005_add_group_permissions.py diff --git a/README.rst b/README.rst index 9a0ea8780..61c3ae37c 100644 --- a/README.rst +++ b/README.rst @@ -88,6 +88,9 @@ Available Features mobile signal (LTE/UMTS/GSM `signal strength <#mobile-signal-strength>`_, `signal quality <#mobile-signal-quality>`_, `access technology in use <#mobile-access-technology-in-use>`_) +* Maintains a record of `WiFi sessions <#monitoring-wifi-sessions>`_ with clients' + MAC address and vendor, session start and stop time and connected device + along with other information * Charts can be viewed at resolutions of 1 day, 3 days, a week, a month and a year * Configurable alerts * CSV Export of monitoring data @@ -371,6 +374,11 @@ Configure celery (you may use a different broker if you want): 'task': 'openwisp_monitoring.check.tasks.run_checks', 'schedule': timedelta(minutes=5), }, + # Delete old WifiSession + 'delete_wifi_clients_and_sessions': { + 'task': 'openwisp_monitoring.monitoring.tasks.delete_wifi_clients_and_sessions', + 'schedule': timedelta(days=180), + }, } INSTALLED_APPS.append('djcelery_email') @@ -791,6 +799,54 @@ Mobile Access Technology in use .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/access-technology.png :align: center +Monitoring WiFi Sessions +------------------------ + +OpenWISP Monitoring maintains a record of WiFi sessions created by clients +joined to a radio of managed devices. The WiFi sessions are created +asynchronously from the monitoring data received from the device. + +You can filter both currently open sessions and past sessions by their +*start* or *stop* time or *organization* or *group* of the device clients +are connected to or even directly by a *device* name or ID. + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-session-changelist.png + :align: center + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-session-change.png + :align: center + +You can disable this feature by configuring +`OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED <#openwisp_monitoring_wifi_sessions_enabled>`_ +setting. + +Scheduled deletion of WiFi sessions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OpenWISP Monitoring provides a celery task to automatically delete +WiFi sessions older than a pre-configured number of days. In order to run this +task periodically, you will need to configure ``CELERY_BEAT_SCHEDULE`` setting as shown +in `setup instructions <#setup-integrate-in-an-existing-django-project>`_. + +The celery task takes only one argument, i.e. number of days. You can provide +any number of days in `args` key while configuring ``CELERY_BEAT_SCHEDULE`` setting. + +E.g., if you want WiFi Sessions older than 30 days to get deleted automatically, +then configure ``CELERY_BEAT_SCHEDULE`` as follows: + +.. code-block:: python + + CELERY_BEAT_SCHEDULE = { + 'delete_wifi_clients_and_sessions': { + 'task': 'openwisp_monitoring.monitoring.tasks.delete_wifi_clients_and_sessions', + 'schedule': timedelta(days=1), + 'args': (30,), # Here we have defined 30 instead of 180 as shown in setup instructions + }, + } + +Please refer to `"Periodic Tasks" section of Celery's documentation `_ +to learn more. + Default Alerts / Notifications ------------------------------ @@ -969,6 +1025,18 @@ you can use the following configuration: 'critical': 'offline' } +``OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Setting this to ``False`` will disable `Monitoring Wifi Sessions <#monitoring-wifi-sessions>`_ +feature. + ``OPENWISP_MONITORING_MANAGEMENT_IP_ONLY`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1377,8 +1445,8 @@ An example usage has been shown below. 'scale': [ [[0, '#c13000'], [0.1,'cb7222'], - [0.5,'#deed0e'], - [0.9, '#7db201'], + [0.5,'#deed0e'], + [0.9, '#7db201'], [1, '#498b26']], ], 'map': [ @@ -1981,6 +2049,8 @@ Add the following to your ``settings.py``: # For extending device_monitoring app DEVICE_MONITORING_DEVICEDATA_MODEL = 'YOUR_MODULE_NAME.DeviceData' DEVICE_MONITORING_DEVICEMONITORING_MODEL = 'YOUR_MODULE_NAME.DeviceMonitoring' + DEVICE_MONITORING_WIFICLIENT_MODEL = 'YOUR_MODULE_NAME.WifiClient' + DEVICE_MONITORING_WIFISESSION_MODEL = 'YOUR_MODULE_NAME.WifiSession' Substitute ```` with your actual django app name (also known as ``app_label``). @@ -2028,10 +2098,11 @@ Similarly for ``device_monitoring`` app, you can do it as: .. code-block:: python - from openwisp_monitoring.device.admin import DeviceAdmin + from openwisp_monitoring.device.admin import DeviceAdmin, WifiSessionAdmin DeviceAdmin.list_display.insert(1, 'my_custom_field') DeviceAdmin.ordering = ['-my_custom_field'] + WifiSessionAdmin.fields += ['my_custom_field'] Similarly for ``monitoring`` app, you can do it as: @@ -2074,16 +2145,23 @@ For ``device_monitoring`` app, from django.contrib import admin from openwisp_monitoring.device_monitoring.admin import DeviceAdmin as BaseDeviceAdmin + from openwisp_monitoring.device_monitoring.admin import WifiSessionAdmin as BaseWifiSessionAdmin from swapper import load_model Device = load_model('config', 'Device') + WifiSession = load_model('device_monitoring', 'WifiSession') admin.site.unregister(Device) + admin.site.unregister(WifiSession) @admin.register(Device) class DeviceAdmin(BaseDeviceAdmin): # add your changes here + @admin.register(WifiSession) + class WifiSessionAdmin(BaseWifiSessionAdmin): + # add your changes here + For ``monitoring`` app, .. code-block:: python diff --git a/openwisp_monitoring/device/admin.py b/openwisp_monitoring/device/admin.py index 9b4840e93..2c90df076 100644 --- a/openwisp_monitoring/device/admin.py +++ b/openwisp_monitoring/device/admin.py @@ -5,7 +5,9 @@ from django.contrib.contenttypes.admin import GenericStackedInline from django.contrib.contenttypes.forms import BaseGenericInlineFormSet from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django.forms import ModelForm +from django.templatetags.static import static from django.urls import reverse from django.utils.html import format_html from django.utils.safestring import mark_safe @@ -18,12 +20,16 @@ from swapper import load_model from openwisp_controller.config.admin import DeviceAdmin as BaseDeviceAdmin +from openwisp_users.multitenancy import MultitenantAdminMixin, MultitenantOrgFilter +from openwisp_utils.admin import ReadOnlyAdmin +from openwisp_utils.admin_theme.filters import SimpleInputFilter from ..monitoring.admin import MetricAdmin from ..settings import MONITORING_API_BASEURL, MONITORING_API_URLCONF from . import settings as app_settings DeviceData = load_model('device_monitoring', 'DeviceData') +WifiSession = load_model('device_monitoring', 'WifiSession') DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring') AlertSettings = load_model('monitoring', 'AlertSettings') Chart = load_model('monitoring', 'Chart') @@ -31,6 +37,7 @@ Metric = load_model('monitoring', 'Metric') Notification = load_model('openwisp_notifications', 'Notification') Check = load_model('check', 'Check') +Organization = load_model('openwisp_users', 'Organization') class CheckInlineFormSet(BaseGenericInlineFormSet): @@ -216,3 +223,145 @@ def get_inlines(self, request, obj=None): admin.site.unregister(Device) admin.site.register(Device, DeviceAdmin) + + +class DeviceFilter(SimpleInputFilter): + """ + Filters WifiSession queryset for input device name + or primary key + """ + + parameter_name = 'device' + title = _('device name or ID') + + def queryset(self, request, queryset): + if self.value() is not None: + try: + uuid.UUID(self.value()) + except ValueError: + lookup = Q(device__name=self.value()) + else: + lookup = Q(device_id=self.value()) + return queryset.filter(lookup) + + +class WifiSessionAdmin(MultitenantAdminMixin, ReadOnlyAdmin): + multitenant_parent = 'device' + model = WifiSession + list_display = [ + 'mac_address', + 'vendor', + 'related_organization', + 'related_device', + 'ssid', + 'ht', + 'vht', + 'start_time', + 'get_stop_time', + ] + fields = [ + 'related_organization', + 'mac_address', + 'vendor', + 'related_device', + 'ssid', + 'interface_name', + 'ht', + 'vht', + 'wmm', + 'wds', + 'wps', + 'start_time', + 'get_stop_time', + 'modified', + ] + search_fields = ['wifi_client__mac_address', 'device__name', 'device__mac_address'] + list_filter = [ + ('device__organization', MultitenantOrgFilter), + 'start_time', + 'stop_time', + 'device__group', + DeviceFilter, + ] + + def get_readonly_fields(self, request, obj=None): + fields = super().get_readonly_fields(request, obj) + fields += [ + 'related_organization', + 'mac_address', + 'vendor', + 'ht', + 'vht', + 'wmm', + 'wds', + 'wps', + 'get_stop_time', + 'modified', + 'related_device', + ] + return fields + + def get_queryset(self, request): + return ( + super() + .get_queryset(request) + .select_related( + 'wifi_client', 'device', 'device__organization', 'device__group' + ) + ) + + def _get_boolean_html(self, value): + icon = static('admin/img/icon-{}.svg'.format('yes' if value is True else 'no')) + return mark_safe(f'') + + def ht(self, obj): + return self._get_boolean_html(obj.wifi_client.ht) + + ht.short_description = 'HT' + + def vht(self, obj): + return self._get_boolean_html(obj.wifi_client.vht) + + vht.short_description = 'VHT' + + def wmm(self, obj): + return self._get_boolean_html(obj.wifi_client.wmm) + + wmm.short_description = 'WMM' + + def wds(self, obj): + return self._get_boolean_html(obj.wifi_client.wds) + + wds.short_description = 'WDS' + + def wps(self, obj): + return self._get_boolean_html(obj.wifi_client.wps) + + wps.short_description = 'WPS' + + def get_stop_time(self, obj): + if obj.stop_time is None: + return mark_safe('online') + return obj.stop_time + + get_stop_time.short_description = _('stop time') + + def related_device(self, obj): + app_label = Device._meta.app_label + url = reverse(f'admin:{app_label}_device_change', args=[obj.device_id]) + return mark_safe(f'{obj.device}') + + related_device.short_description = _('device') + + def related_organization(self, obj): + app_label = Organization._meta.app_label + url = reverse( + f'admin:{app_label}_organization_change', args=[obj.organization.id] + ) + return mark_safe(f'{obj.organization}') + + related_organization.short_description = _('organization') + + +if app_settings.WIFI_SESSIONS_ENABLED: + admin.site.register(WifiSession, WifiSessionAdmin) diff --git a/openwisp_monitoring/device/apps.py b/openwisp_monitoring/device/apps.py index af12a9d39..7297d02ad 100644 --- a/openwisp_monitoring/device/apps.py +++ b/openwisp_monitoring/device/apps.py @@ -3,10 +3,11 @@ from django.apps import AppConfig from django.conf import settings from django.core.cache import cache +from django.db.models import Case, Count, Sum, When from django.db.models.signals import post_delete, post_save from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from swapper import load_model +from swapper import get_model_name, load_model from openwisp_controller.config.signals import checksum_requested, config_status_changed from openwisp_controller.connection import settings as connection_settings @@ -16,6 +17,7 @@ register_dashboard_chart, register_dashboard_template, ) +from openwisp_utils.admin_theme.menu import register_menu_subitem from ..check import settings as check_settings from ..settings import MONITORING_API_BASEURL, MONITORING_API_URLCONF @@ -35,9 +37,11 @@ def ready(self): self.connect_is_working_changed() self.connect_device_signals() self.connect_config_status_changed() + self.connect_offline_device_close_wifisession() self.device_recovery_detection() self.set_update_config_model() self.register_dashboard_items() + self.register_menu_groups() self.add_connection_ignore_notification_reasons() def connect_device_signals(self): @@ -166,6 +170,19 @@ def connect_config_status_changed(cls): dispatch_uid='monitoring.config_status_changed_receiver', ) + @classmethod + def connect_offline_device_close_wifisession(cls): + if not app_settings.WIFI_SESSIONS_ENABLED: + return + + DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring') + WifiSession = load_model('device_monitoring', 'WifiSession') + health_status_changed.connect( + WifiSession.offline_device_close_session, + sender=DeviceMonitoring, + dispatch_uid='offline_device_close_session', + ) + @classmethod def config_status_changed_receiver(cls, sender, instance, **kwargs): from ..check.tasks import perform_check @@ -244,6 +261,65 @@ def register_dashboard_items(self): }, ) + if app_settings.WIFI_SESSIONS_ENABLED: + WifiSession = load_model('device_monitoring', 'WifiSession') + register_dashboard_chart( + position=13, + config={ + 'name': _('Currently Active WiFi Sessions'), + 'query_params': { + 'app_label': WifiSession._meta.app_label, + 'model': WifiSession._meta.model_name, + 'annotate': { + 'active': Count( + Case( + When( + stop_time__isnull=True, + then=1, + ) + ) + ), + }, + 'aggregate': { + 'active__sum': Sum('active'), + }, + }, + 'filters': { + 'key': 'stop_time__isnull', + 'active__sum': 'true', + }, + 'colors': { + 'active__sum': '#267126', + }, + 'labels': { + 'active__sum': _('Currently Active WiFi Sessions'), + }, + 'quick_link': { + 'url': reverse_lazy( + 'admin:{app_label}_{model_name}_changelist'.format( + app_label=WifiSession._meta.app_label, + model_name=WifiSession._meta.model_name, + ) + ), + 'label': _('Open WiFi session list'), + 'custom_css_classes': ['negative-top-20'], + }, + }, + ) + + def register_menu_groups(self): + if app_settings.WIFI_SESSIONS_ENABLED: + register_menu_subitem( + group_position=80, + item_position=0, + config={ + 'label': _('WiFi Sessions'), + 'model': get_model_name('device_monitoring', 'WifiSession'), + 'name': 'changelist', + 'icon': 'ow-monitoring-wifi', + }, + ) + def add_connection_ignore_notification_reasons(self): ConnectionConfig._ignore_connection_notification_reasons.extend( ['timed out', 'Unable to connect'] diff --git a/openwisp_monitoring/device/base/models.py b/openwisp_monitoring/device/base/models.py index b632ff397..20395d9fe 100644 --- a/openwisp_monitoring/device/base/models.py +++ b/openwisp_monitoring/device/base/models.py @@ -20,12 +20,14 @@ from pytz import timezone as tz from swapper import load_model +from openwisp_controller.config.validators import mac_address_validator from openwisp_utils.base import TimeStampedEditableModel from ...db import device_data_query, timeseries_db from ...monitoring.signals import threshold_crossed from ...monitoring.tasks import timeseries_write from .. import settings as app_settings +from .. import tasks from ..schema import schema from ..signals import health_status_changed from ..utils import SHORT_RP, get_device_cache_key @@ -210,6 +212,10 @@ def save_data(self, time=None): ], timeout=86400, # 24 hours ) + if app_settings.WIFI_SESSIONS_ENABLED: + tasks.save_wifi_clients_and_sessions.delay( + device_data=self.data, device_pk=self.pk + ) def json(self, *args, **kwargs): return json.dumps(self.data, *args, **kwargs) @@ -311,3 +317,79 @@ def is_metric_critical(metric): ): return True return False + + +class AbstractWifiClient(TimeStampedEditableModel): + id = None + mac_address = models.CharField( + max_length=17, + db_index=True, + primary_key=True, + validators=[mac_address_validator], + help_text=_('MAC address'), + ) + vendor = models.CharField(max_length=200, blank=True, null=True) + ht = models.BooleanField(default=False, verbose_name='HT') + vht = models.BooleanField(default=False, verbose_name='VHT') + wmm = models.BooleanField(default=False, verbose_name='WMM') + wds = models.BooleanField(default=False, verbose_name='WDS') + wps = models.BooleanField(default=False, verbose_name='WPS') + + class Meta: + abstract = True + verbose_name = _('WiFi Client') + + +class AbstractWifiSession(TimeStampedEditableModel): + created = None + + device = models.ForeignKey( + swapper.get_model_name('config', 'Device'), + on_delete=models.CASCADE, + ) + wifi_client = models.ForeignKey( + swapper.get_model_name('device_monitoring', 'WifiClient'), + on_delete=models.CASCADE, + ) + ssid = models.CharField( + max_length=32, blank=True, null=True, verbose_name=_('SSID') + ) + interface_name = models.CharField( + max_length=15, + ) + start_time = models.DateTimeField( + verbose_name=_('start time'), + db_index=True, + auto_now=True, + ) + stop_time = models.DateTimeField( + verbose_name=_('stop time'), + db_index=True, + null=True, + blank=True, + ) + + class Meta: + abstract = True + verbose_name = _('WiFi Session') + ordering = ('-start_time',) + + def __str__(self): + return self.mac_address + + @property + def mac_address(self): + return self.wifi_client.mac_address + + @property + def vendor(self): + return self.wifi_client.vendor + + @property + def organization(self): + return self.device.organization + + @classmethod + def offline_device_close_session(cls, instance, *args, **kwargs): + if kwargs['status'] == 'critical': + tasks.offline_device_close_session.delay(device_id=instance.device_id) diff --git a/openwisp_monitoring/device/migrations/0004_wificlient_wifisession.py b/openwisp_monitoring/device/migrations/0004_wificlient_wifisession.py new file mode 100644 index 000000000..d7be5e448 --- /dev/null +++ b/openwisp_monitoring/device/migrations/0004_wificlient_wifisession.py @@ -0,0 +1,136 @@ +# Generated by Django 4.0.4 on 2022-05-17 11:28 + +import re +import uuid + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import swapper +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + swapper.dependency( + *swapper.split(settings.AUTH_USER_MODEL), version='0004_default_groups' + ), + swapper.dependency('config', 'Device'), + ('device_monitoring', '0003_update_template'), + ] + + operations = [ + migrations.CreateModel( + name='WifiClient', + fields=[ + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'mac_address', + models.CharField( + db_index=True, + help_text='MAC address', + max_length=17, + primary_key=True, + serialize=False, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), + ), + ('vendor', models.CharField(blank=True, max_length=200, null=True)), + ('ht', models.BooleanField(default=False, verbose_name='HT')), + ('vht', models.BooleanField(default=False, verbose_name='VHT')), + ('wmm', models.BooleanField(default=False, verbose_name='WMM')), + ('wds', models.BooleanField(default=False, verbose_name='WDS')), + ('wps', models.BooleanField(default=False, verbose_name='WPS')), + ], + options={ + 'verbose_name': 'WiFi Client', + 'abstract': False, + 'swappable': 'DEVICE_MONITORING_WIFICLIENT_MODEL', + }, + ), + migrations.CreateModel( + name='WifiSession', + fields=[ + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'ssid', + models.CharField( + blank=True, max_length=32, null=True, verbose_name='SSID' + ), + ), + ('interface_name', models.CharField(max_length=15)), + ( + 'start_time', + models.DateTimeField( + auto_now=True, db_index=True, verbose_name='start time' + ), + ), + ( + 'stop_time', + models.DateTimeField( + blank=True, db_index=True, null=True, verbose_name='stop time' + ), + ), + ( + 'device', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name('config', 'Device'), + ), + ), + ( + 'wifi_client', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name('device_monitoring', 'WifiClient'), + ), + ), + ], + options={ + 'verbose_name': 'WiFi Session', + 'ordering': ('-start_time',), + 'abstract': False, + 'swappable': 'DEVICE_MONITORING_WIFISESSION_MODEL', + }, + ), + ] diff --git a/openwisp_monitoring/device/migrations/0005_add_group_permissions.py b/openwisp_monitoring/device/migrations/0005_add_group_permissions.py new file mode 100644 index 000000000..d5f76020a --- /dev/null +++ b/openwisp_monitoring/device/migrations/0005_add_group_permissions.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-05-17 11:28 + +from django.db import migrations + +from . import assign_permissions_to_groups + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_monitoring', '0004_wificlient_wifisession'), + ] + + operations = [ + migrations.RunPython( + assign_permissions_to_groups, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/openwisp_monitoring/device/migrations/__init__.py b/openwisp_monitoring/device/migrations/__init__.py index aa84072fd..f230042ba 100644 --- a/openwisp_monitoring/device/migrations/__init__.py +++ b/openwisp_monitoring/device/migrations/__init__.py @@ -1,5 +1,9 @@ from collections import OrderedDict +from django.contrib.auth.models import Permission + +from openwisp_controller.migrations import create_default_permissions, get_swapped_model + # Use a pre-defined UUID so the template can be upgraded via migration scripts if needed TEMPLATE_MONITORING_UUID = '00000000-defa-defa-defa-000000000000' TEMPLATE_OPENWISP_MONITORING_01 = OrderedDict( @@ -86,3 +90,36 @@ "contents": "#!/bin/sh\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n/usr/sbin/legacy-update-openwisp-packages\n/usr/sbin/legacy-openwisp-monitoring\n", # noqa } ) + + +def assign_permissions_to_groups(apps, schema_editor): + create_default_permissions(apps, schema_editor) + operators_read_only_admins_manage = [ + 'devicedata', + 'devicemonitoring', + 'wificlient', + 'wifisession', + ] + manage_operations = ['add', 'change', 'delete'] + Group = get_swapped_model(apps, 'openwisp_users', 'Group') + + try: + admin = Group.objects.get(name='Administrator') + operator = Group.objects.get(name='Operator') + # consider failures custom cases + # that do not have to be dealt with + except Group.DoesNotExist: + return + + for model_name in operators_read_only_admins_manage: + try: + permission = Permission.objects.get(codename='view_{}'.format(model_name)) + operator.permissions.add(permission.pk) + except Permission.DoesNotExist: + pass + for operation in manage_operations: + admin.permissions.add( + Permission.objects.get( + codename='{}_{}'.format(operation, model_name) + ).pk + ) diff --git a/openwisp_monitoring/device/models.py b/openwisp_monitoring/device/models.py index 54d56715c..e95b6731a 100644 --- a/openwisp_monitoring/device/models.py +++ b/openwisp_monitoring/device/models.py @@ -1,7 +1,12 @@ from django.contrib.contenttypes.fields import GenericRelation from swapper import get_model_name, load_model, swappable_setting -from .base.models import AbstractDeviceData, AbstractDeviceMonitoring +from .base.models import ( + AbstractDeviceData, + AbstractDeviceMonitoring, + AbstractWifiClient, + AbstractWifiSession, +) BaseDevice = load_model('config', 'Device', require_ready=False) @@ -19,3 +24,15 @@ class DeviceMonitoring(AbstractDeviceMonitoring): class Meta(AbstractDeviceMonitoring.Meta): abstract = False swappable = swappable_setting('device_monitoring', 'DeviceMonitoring') + + +class WifiClient(AbstractWifiClient): + class Meta(AbstractWifiClient.Meta): + abstract = False + swappable = swappable_setting('device_monitoring', 'WifiClient') + + +class WifiSession(AbstractWifiSession): + class Meta(AbstractWifiSession.Meta): + abstract = False + swappable = swappable_setting('device_monitoring', 'WifiSession') diff --git a/openwisp_monitoring/device/settings.py b/openwisp_monitoring/device/settings.py index 2270201fa..8a8250552 100644 --- a/openwisp_monitoring/device/settings.py +++ b/openwisp_monitoring/device/settings.py @@ -50,3 +50,4 @@ def get_health_status_labels(): DEVICE_RECOVERY_DETECTION = get_settings_value('DEVICE_RECOVERY_DETECTION', True) MAC_VENDOR_DETECTION = get_settings_value('MAC_VENDOR_DETECTION', True) DASHBOARD_MAP = get_settings_value('DASHBOARD_MAP', True) +WIFI_SESSIONS_ENABLED = get_settings_value('WIFI_SESSIONS_ENABLED', True) diff --git a/openwisp_monitoring/device/tasks.py b/openwisp_monitoring/device/tasks.py index 731e9e1d4..40c02cf57 100644 --- a/openwisp_monitoring/device/tasks.py +++ b/openwisp_monitoring/device/tasks.py @@ -2,6 +2,7 @@ from celery import shared_task from django.core.exceptions import ObjectDoesNotExist +from django.utils.timezone import now, timedelta from swapper import load_model from ..check.tasks import perform_check @@ -31,3 +32,68 @@ def trigger_device_checks(pk, recovery=True): if not has_checks: status = 'ok' if recovery else 'critical' device.monitoring.update_status(status) + + +@shared_task +def save_wifi_clients_and_sessions(device_data, device_pk): + _WIFICLIENT_FIELDS = ['vendor', 'ht', 'vht', 'wmm', 'wds', 'wps'] + WifiClient = load_model('device_monitoring', 'WifiClient') + WifiSession = load_model('device_monitoring', 'WifiSession') + + active_sessions = [] + interfaces = device_data.get('interfaces', []) + for interface in interfaces: + if interface.get('type') != 'wireless': + continue + interface_name = interface.get('name') + wireless = interface.get('wireless', {}) + + ssid = wireless.get('ssid') + clients = wireless.get('clients', []) + for client in clients: + # Save WifiClient + client_obj, created = WifiClient.objects.get_or_create( + mac_address=client.get('mac') + ) + update_fields = [] + for field in _WIFICLIENT_FIELDS: + if getattr(client_obj, field) != client.get(field): + setattr(client_obj, field, client.get(field)) + update_fields.append(field) + if update_fields: + client_obj.full_clean() + client_obj.save(update_fields=update_fields) + + # Save WifiSession + session_obj, _ = WifiSession.objects.get_or_create( + device_id=device_pk, + interface_name=interface_name, + ssid=ssid, + wifi_client=client_obj, + stop_time=None, + ) + active_sessions.append(session_obj.pk) + + # Close open WifiSession + WifiSession.objects.filter(device_id=device_pk, stop_time=None,).exclude( + pk__in=active_sessions + ).update(stop_time=now()) + + +@shared_task +def delete_wifi_clients_and_sessions(days=6 * 30): + WifiClient = load_model('device_monitoring', 'WifiClient') + WifiSession = load_model('device_monitoring', 'WifiSession') + + WifiSession.objects.filter(start_time__lte=(now() - timedelta(days=days))).delete() + WifiClient.objects.exclude( + mac_address__in=WifiSession.objects.values_list('wifi_client') + ).delete() + + +@shared_task +def offline_device_close_session(device_id): + WifiSession = load_model('device_monitoring', 'WifiSession') + WifiSession.objects.filter(device_id=device_id, stop_time__isnull=True).update( + stop_time=now() + ) diff --git a/openwisp_monitoring/device/tests/__init__.py b/openwisp_monitoring/device/tests/__init__.py index 33850d058..6d439b8d1 100644 --- a/openwisp_monitoring/device/tests/__init__.py +++ b/openwisp_monitoring/device/tests/__init__.py @@ -13,6 +13,8 @@ Metric = load_model('monitoring', 'Metric') DeviceData = load_model('device_monitoring', 'DeviceData') +WifiClient = load_model('device_monitoring', 'WifiClient') +WifiSession = load_model('device_monitoring', 'WifiSession') Chart = load_model('monitoring', 'Chart') Config = load_model('config', 'Config') Device = load_model('config', 'Device') @@ -282,3 +284,53 @@ class DeviceMonitoringTransactionTestcase( TestDeviceMonitoringMixin, TransactionTestCase ): pass + + +class TestWifiClientSessionMixin(TestDeviceMonitoringMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + manage_short_retention_policy() + + @property + def _sample_data(self): + data = deepcopy(self._data()) + data.pop('resources') + return data + + def _create_device_data(self, device=None): + device = device or self._create_device() + return DeviceData(pk=device.pk) + + def _save_device_data(self, device_data=None, data=None): + dd = device_data or self._create_device_data() + dd.data = data or self._sample_data + dd.save_data() + return dd + + def _create_wifi_client(self, **kwargs): + options = { + 'mac_address': '22:33:44:55:66:77', + 'vendor': '', + 'ht': True, + 'vht': True, + 'wmm': False, + 'wds': False, + 'wps': False, + } + options.update(**kwargs) + wifi_client = WifiClient(**options) + wifi_client.full_clean() + wifi_client.save() + return wifi_client + + def _create_wifi_session(self, **kwargs): + if 'wifi_client' not in kwargs: + kwargs['wifi_client'] = self._create_wifi_client() + if 'device' not in kwargs: + kwargs['device'] = self._create_device() + options = {'ssid': 'Free Public WiFi', 'interface_name': 'wlan0'} + options.update(kwargs) + wifi_session = WifiSession(**options) + wifi_session.full_clean() + wifi_session.save() diff --git a/openwisp_monitoring/device/tests/test_admin.py b/openwisp_monitoring/device/tests/test_admin.py index da55d19b6..82872a85c 100644 --- a/openwisp_monitoring/device/tests/test_admin.py +++ b/openwisp_monitoring/device/tests/test_admin.py @@ -1,18 +1,26 @@ +from copy import deepcopy + from django.contrib.auth import get_user_model from django.contrib.contenttypes.forms import generic_inlineformset_factory +from django.test import TestCase from django.urls import reverse -from django.utils.timezone import now +from django.utils.timezone import now, timedelta +from freezegun import freeze_time from swapper import get_model_name, load_model +from openwisp_controller.config.tests.utils import CreateDeviceGroupMixin from openwisp_controller.geo.tests.utils import TestGeoMixin +from openwisp_users.tests.utils import TestMultitenantAdminMixin from ...check.settings import CHECK_CLASSES from ..admin import CheckInline, CheckInlineFormSet -from . import DeviceMonitoringTestCase +from . import DeviceMonitoringTestCase, TestWifiClientSessionMixin Chart = load_model('monitoring', 'Chart') Metric = load_model('monitoring', 'Metric') DeviceData = load_model('device_monitoring', 'DeviceData') +WifiClient = load_model('device_monitoring', 'WifiClient') +WifiSession = load_model('device_monitoring', 'WifiSession') User = get_user_model() Check = load_model('check', 'Check') # needed for config.geo @@ -310,3 +318,154 @@ def test_dashboard(self): self.assertContains(response, static_file) self.assertContains(response, 'Monitoring Status') self.assertContains(response, '#267126') + + +class TestWifiSessionAdmin( + CreateDeviceGroupMixin, + TestMultitenantAdminMixin, + TestWifiClientSessionMixin, + TestCase, +): + def setUp(self): + admin = self._create_admin() + self.client.force_login(admin) + + def test_changelist_filters_and_multitenancy(self): + url = reverse( + 'admin:{app_label}_{model_name}_changelist'.format( + app_label=WifiSession._meta.app_label, + model_name=WifiSession._meta.model_name, + ) + ) + org1 = self._create_org(name='org1', slug='org1') + org1_device_group = self._create_device_group( + name='Org1 Routers', organization=org1 + ) + org1_device = self._create_device( + name='org1-device', organization=org1, group=org1_device_group + ) + org1_dd = self._create_device_data(device=org1_device) + org1_interface_data = deepcopy(self._sample_data['interfaces'][0]) + org1_interface_data['name'] = 'org1_wlan0' + org1_interface_data['wireless']['ssid'] = 'org1_wifi' + org1_interface_data['wireless']['clients'][0]['mac'] = '00:ee:ad:34:f5:3b' + + org2 = self._create_org(name='org1', slug='org2') + org2_device_group = self._create_device_group( + name='Org2 Routers', organization=org2 + ) + org2_device = self._create_device( + name='org2-device', organization=org2, group=org2_device_group + ) + org2_dd = self._create_device_data(device=org2_device) + org2_interface_data = deepcopy(self._sample_data['interfaces'][0]) + org2_interface_data['name'] = 'org2_wlan0' + org2_interface_data['wireless']['ssid'] = 'org2_wifi' + org2_interface_data['wireless']['clients'][0]['mac'] = '00:ee:ad:34:f5:3c' + + self._save_device_data( + device_data=org1_dd, + data={'type': 'DeviceMonitoring', 'interfaces': [org1_interface_data]}, + ) + self.assertEqual(WifiClient.objects.count(), 1) + self.assertEqual( + WifiSession.objects.filter(device__organization=org1).count(), 1 + ) + WifiSession.objects.filter(device__organization=org1).update(stop_time=now()) + + with freeze_time(now() - timedelta(days=2)): + self._save_device_data( + device_data=org2_dd, + data={'type': 'DeviceMonitoring', 'interfaces': [org2_interface_data]}, + ) + self.assertEqual(WifiClient.objects.count(), 2) + self.assertEqual( + WifiSession.objects.filter(device__organization=org2).count(), 1 + ) + + def _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ): + self.assertContains( + response, '

\n\n1 WiFi Session\n\n\n

' + ) + self.assertContains( + response, + '{}'.format( + org2_interface_data['wireless']['ssid'] + ), + ) + self.assertContains( + response, org2_interface_data['wireless']['clients'][0]['mac'] + ) + self.assertNotContains( + response, + '{}'.format( + org1_interface_data['wireless']['ssid'] + ), + ) + self.assertNotContains( + response, org1_interface_data['wireless']['clients'][0]['mac'] + ) + + with self.subTest('Test without filters'): + response = self.client.get(url) + self.assertContains( + response, '

\n\n2 WiFi Sessions\n\n\n

' + ) + + with self.subTest('Test start_time filter'): + response = self.client.get( + url, {'start_time__lte': now() - timedelta(days=1)} + ) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + + with self.subTest('Test stop_time filter'): + response = self.client.get(url, {'stop_time__isnull': 'True'}) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + + with self.subTest('Test organization filter'): + response = self.client.get( + url, {'device__organization__id__exact': str(org2.pk)} + ) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + + with self.subTest('Test device_group filter'): + response = self.client.get( + url, {'device__group__id__exact': str(org2_device_group.pk)} + ) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + + with self.subTest('Test device filter'): + # Filter by device name + response = self.client.get(url, {'device': org2_device.name}) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + # Filter by device pk + response = self.client.get(url, {'device': str(org2_device.pk)}) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + + with self.subTest('Test multitenancy'): + administrator = self._create_administrator(organizations=[org2]) + self.client.force_login(administrator) + _assert_org2_wifi_session_in_response( + response, org1_interface_data, org2_interface_data + ) + + def test_wifi_session_chart_on_index(self): + url = reverse('admin:index') + self._create_wifi_session() + response = self.client.get(url) + self.assertContains(response, 'Currently Active WiFi Sessions') + self.assertContains(response, 'Open WiFi session list') diff --git a/openwisp_monitoring/device/tests/test_models.py b/openwisp_monitoring/device/tests/test_models.py index e348116d4..8b917bcea 100644 --- a/openwisp_monitoring/device/tests/test_models.py +++ b/openwisp_monitoring/device/tests/test_models.py @@ -4,6 +4,9 @@ from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.test import TestCase +from django.utils.timezone import now, timedelta +from freezegun import freeze_time from swapper import load_model from openwisp_controller.connection.tasks import update_config @@ -11,13 +14,16 @@ from openwisp_utils.tests import catch_signal from ...db import timeseries_db +from .. import settings as app_settings from ..signals import health_status_changed -from ..tasks import trigger_device_checks +from ..tasks import delete_wifi_clients_and_sessions, trigger_device_checks from ..utils import get_device_cache_key -from . import DeviceMonitoringTestCase +from . import DeviceMonitoringTestCase, TestWifiClientSessionMixin DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring') DeviceData = load_model('device_monitoring', 'DeviceData') +WifiClient = load_model('device_monitoring', 'WifiClient') +WifiSession = load_model('device_monitoring', 'WifiSession') class BaseTestCase(DeviceMonitoringTestCase): @@ -642,3 +648,137 @@ def test_unknown_critical(self): ping.write(0) dm.refresh_from_db() self.assertEqual(dm.status, 'critical') + + +class TestWifiClientSession(TestWifiClientSessionMixin, TestCase): + wifi_client_model = WifiClient + wifi_session_model = WifiSession + device_data_model = DeviceData + + def test_wifi_client_session_created(self): + device_data = self._save_device_data() + self.assertEqual(WifiClient.objects.count(), 3) + self.assertEqual(WifiSession.objects.count(), 3) + wifi_client1 = WifiClient.objects.get(mac_address='00:ee:ad:34:f5:3b') + self.assertEqual(wifi_client1.vendor, None) + self.assertEqual(wifi_client1.ht, True) + self.assertEqual(wifi_client1.vht, False) + self.assertEqual(wifi_client1.wmm, True) + self.assertEqual(wifi_client1.wds, False) + self.assertEqual(wifi_client1.wps, False) + + wifi_client2 = WifiClient.objects.get(mac_address='b0:e1:7e:30:16:44') + self.assertEqual(wifi_client2.vendor, None) + self.assertEqual(wifi_client2.ht, True) + self.assertEqual(wifi_client2.vht, False) + self.assertEqual(wifi_client2.wmm, True) + self.assertEqual(wifi_client2.wds, False) + self.assertEqual(wifi_client2.wps, False) + + wifi_client3 = WifiClient.objects.get(mac_address='c0:ee:fb:34:f5:4b') + self.assertEqual(wifi_client3.vendor, None) + self.assertEqual(wifi_client3.ht, True) + self.assertEqual(wifi_client3.vht, False) + self.assertEqual(wifi_client3.wmm, True) + self.assertEqual(wifi_client3.wds, False) + self.assertEqual(wifi_client3.wps, False) + + wifi_client1_session = WifiSession.objects.get(wifi_client=wifi_client1) + self.assertEqual(wifi_client1_session.device, device_data) + self.assertEqual(wifi_client1_session.ssid, 'testnet') + self.assertEqual(wifi_client1_session.interface_name, 'wlan0') + self.assertNotEqual(wifi_client1_session.start_time, None) + self.assertEqual(wifi_client1_session.stop_time, None) + + wifi_client2_session = WifiSession.objects.get(wifi_client=wifi_client2) + self.assertEqual(wifi_client2_session.device, device_data) + self.assertEqual(wifi_client2_session.ssid, 'testnet') + self.assertEqual(wifi_client2_session.interface_name, 'wlan1') + self.assertNotEqual(wifi_client2_session.start_time, None) + self.assertEqual(wifi_client2_session.stop_time, None) + + wifi_client3_session = WifiSession.objects.get(wifi_client=wifi_client3) + self.assertEqual(wifi_client3_session.device, device_data) + self.assertEqual(wifi_client3_session.ssid, 'testnet') + self.assertEqual(wifi_client3_session.interface_name, 'wlan1') + self.assertNotEqual(wifi_client3_session.start_time, None) + self.assertEqual(wifi_client3_session.stop_time, None) + + def test_updating_existing_wifi_sessions(self): + device_data = self._create_device_data() + self._save_device_data(device_data=device_data) + self.assertEqual(WifiClient.objects.count(), 3) + self.assertEqual(WifiSession.objects.count(), 3) + + self._save_device_data(device_data=device_data) + self.assertEqual(WifiClient.objects.count(), 3) + self.assertEqual(WifiSession.objects.count(), 3) + + def test_opening_closing_existing_sessions(self): + data = deepcopy(self._sample_data) + device_data = self._create_device_data() + self._save_device_data(device_data, data) + self.assertEqual(WifiSession.objects.filter(stop_time=None).count(), 3) + self.assertEqual(WifiSession.objects.count(), 3) + self.assertEqual(WifiClient.objects.count(), 3) + + with self.subTest('Test closing session'): + self._save_device_data( + device_data, data={'type': 'DeviceMonitoring', 'interfaces': []} + ) + self.assertEqual(WifiSession.objects.filter(stop_time=None).count(), 0) + self.assertEqual(WifiSession.objects.count(), 3) + self.assertEqual(WifiClient.objects.count(), 3) + + with self.subTest('Test re-opening session for exising clients'): + self._save_device_data(device_data, data) + self.assertEqual(WifiSession.objects.filter(stop_time=None).count(), 3) + self.assertEqual(WifiSession.objects.count(), 6) + self.assertEqual(WifiClient.objects.count(), 3) + + def test_delete_old_wifi_sessions(self): + with freeze_time(now() - timedelta(days=6 * 31)): + self._create_wifi_session() + self.assertEqual(WifiSession.objects.count(), 1) + delete_wifi_clients_and_sessions.delay() + self.assertEqual(WifiSession.objects.count(), 0) + + def test_database_queries(self): + data = deepcopy(self._sample_data) + device_data = self._create_device_data() + with self.subTest('Test creating new clients and sessions'): + with self.assertNumQueries(28): + self._save_device_data(device_data, data) + + with self.subTest('Test updating existing clients and sessions'): + with self.assertNumQueries(7): + self._save_device_data(device_data, data) + + with self.subTest('Test closing existing sessions'): + with self.assertNumQueries(1): + self._save_device_data( + device_data, data={'type': 'DeviceMonitoring', 'interface': []} + ) + + with self.subTest('Test new sessions for existing clients'): + with self.assertNumQueries(16): + self._save_device_data(device_data, data) + + @patch.object(app_settings, 'WIFI_SESSIONS_ENABLED', False) + def test_disabling_wifi_sessions(self): + device_data = self._create_device_data() + with self.assertNumQueries(0): + self._save_device_data(device_data) + self.assertEqual(WifiClient.objects.count(), 0) + self.assertEqual(WifiSession.objects.count(), 0) + + def test_device_offline_close_session(self): + device_monitoring = self._create_device_monitoring() + wifi_client = self._create_wifi_client() + self._create_wifi_session( + wifi_client=wifi_client, device=device_monitoring.device + ) + self.assertEqual(WifiSession.objects.filter(stop_time__isnull=True).count(), 1) + device_monitoring.update_status('critical') + self.assertEqual(WifiSession.objects.filter(stop_time__isnull=True).count(), 0) + self.assertEqual(WifiSession.objects.count(), 1) diff --git a/openwisp_monitoring/monitoring/admin.py b/openwisp_monitoring/monitoring/admin.py index 3b0d721bb..1bc633d26 100644 --- a/openwisp_monitoring/monitoring/admin.py +++ b/openwisp_monitoring/monitoring/admin.py @@ -9,6 +9,9 @@ Chart = load_model('monitoring', 'Chart') Metric = load_model('monitoring', 'Metric') AlertSettings = load_model('monitoring', 'AlertSettings') +WifiSession = load_model('device_monitoring', 'WifiSession') +Device = load_model('config', 'Device') +Organization = load_model('openwisp_users', 'Organization') class AlertSettingsForm(ModelForm): diff --git a/openwisp_monitoring/monitoring/migrations/__init__.py b/openwisp_monitoring/monitoring/migrations/__init__.py index e69de29bb..d22e24e27 100644 --- a/openwisp_monitoring/monitoring/migrations/__init__.py +++ b/openwisp_monitoring/monitoring/migrations/__init__.py @@ -0,0 +1,36 @@ +from django.contrib.auth.models import Permission + +from openwisp_controller.migrations import create_default_permissions, get_swapped_model + + +def assign_permissions_to_groups(apps, schema_editor): + create_default_permissions(apps, schema_editor) + operators_read_only_admins_manage = [ + 'check', + 'alertsettings', + 'wificlient', + 'wifisession', + ] + manage_operations = ['add', 'change', 'delete'] + Group = get_swapped_model(apps, 'openwisp_users', 'Group') + + try: + admin = Group.objects.get(name='Administrator') + operator = Group.objects.get(name='Operator') + # consider failures custom cases + # that do not have to be dealt with + except Group.DoesNotExist: + return + + for model_name in operators_read_only_admins_manage: + try: + permission = Permission.objects.get(codename='view_{}'.format(model_name)) + operator.permissions.add(permission.pk) + except Permission.DoesNotExist: + pass + for operation in manage_operations: + admin.permissions.add( + Permission.objects.get( + codename='{}_{}'.format(operation, model_name) + ).pk + ) diff --git a/requirements-test.txt b/requirements-test.txt index e89e5b08a..686c61121 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -openwisp-utils[qa]~=1.0.1 +openwisp-utils[qa] @ https://github.com/openwisp/openwisp-utils/tarball/master redis~=3.5.3 django-redis~=4.12.1 mock-ssh-server~=0.9.0 diff --git a/requirements.txt b/requirements.txt index 04c515ef9..b6bd3450e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ django-cache-memoize~=0.1.0 django-nested-admin~=3.4.0 netaddr~=0.8 python-dateutil>=2.7,<3.0 +openwisp-utils[rest] @ https://github.com/openwisp/openwisp-utils/tarball/master diff --git a/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py b/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py index 9962adea2..5af74326b 100644 --- a/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py +++ b/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py @@ -1,5 +1,6 @@ # Generated by Django 3.0.5 on 2020-05-27 03:06 +import re import uuid import django.db.models.deletion @@ -90,4 +91,120 @@ class Migration(migrations.Migration): ], options={'abstract': False}, ), + migrations.CreateModel( + name='WifiClient', + fields=[ + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'mac_address', + models.CharField( + db_index=True, + help_text='MAC address', + max_length=17, + primary_key=True, + serialize=False, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), + ), + ('vendor', models.CharField(blank=True, max_length=200, null=True)), + ('ht', models.BooleanField(default=False, verbose_name='HT')), + ('vht', models.BooleanField(default=False, verbose_name='VHT')), + ('wmm', models.BooleanField(default=False, verbose_name='WMM')), + ('wds', models.BooleanField(default=False, verbose_name='WDS')), + ('wps', models.BooleanField(default=False, verbose_name='WPS')), + ( + 'details', + models.CharField(default='devicemonitoring', max_length=64), + ), + ], + options={ + 'verbose_name': 'WiFi Client', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='WifiSession', + fields=[ + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'ssid', + models.CharField( + blank=True, max_length=32, null=True, verbose_name='SSID' + ), + ), + ('interface_name', models.CharField(max_length=15)), + ( + 'start_time', + models.DateTimeField( + auto_now=True, db_index=True, verbose_name='start time' + ), + ), + ( + 'stop_time', + models.DateTimeField( + blank=True, db_index=True, null=True, verbose_name='stop time' + ), + ), + ( + 'details', + models.CharField(default='devicemonitoring', max_length=64), + ), + ( + 'device', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=swapper.get_model_name('config', 'Device'), + ), + ), + ( + 'wifi_client', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='sample_device_monitoring.wificlient', + ), + ), + ], + options={ + 'verbose_name': 'WiFi Session', + 'abstract': False, + 'ordering': ('-start_time',), + }, + ), ] diff --git a/tests/openwisp2/sample_device_monitoring/models.py b/tests/openwisp2/sample_device_monitoring/models.py index 51fc7ea08..323b66339 100644 --- a/tests/openwisp2/sample_device_monitoring/models.py +++ b/tests/openwisp2/sample_device_monitoring/models.py @@ -5,11 +5,20 @@ from openwisp_monitoring.device.base.models import ( AbstractDeviceData, AbstractDeviceMonitoring, + AbstractWifiClient, + AbstractWifiSession, ) BaseDevice = load_model('config', 'Device', require_ready=False) +class DetailsModel(models.Model): + details = models.CharField(max_length=64, default='devicemonitoring') + + class Meta: + abstract = True + + class DeviceData(AbstractDeviceData, BaseDevice): checks = GenericRelation(get_model_name('check', 'Check')) metrics = GenericRelation(get_model_name('monitoring', 'Metric')) @@ -18,11 +27,19 @@ class Meta: proxy = True -class DeviceMonitoring(AbstractDeviceMonitoring): - details = models.CharField(max_length=64, default='devicemonitoring') - +class DeviceMonitoring(DetailsModel, AbstractDeviceMonitoring): class Meta(AbstractDeviceMonitoring.Meta): abstract = False def __str__(self): return self.details + + +class WifiClient(DetailsModel, AbstractWifiClient): + class Meta(AbstractWifiClient.Meta): + abstract = False + + +class WifiSession(DetailsModel, AbstractWifiSession): + class Meta(AbstractWifiSession.Meta): + abstract = False diff --git a/tests/openwisp2/sample_device_monitoring/tests.py b/tests/openwisp2/sample_device_monitoring/tests.py index da1823eb6..86bc0799a 100644 --- a/tests/openwisp2/sample_device_monitoring/tests.py +++ b/tests/openwisp2/sample_device_monitoring/tests.py @@ -2,6 +2,9 @@ DeviceMonitoringTestCase as DeviceMonitoringTestCase, ) from openwisp_monitoring.device.tests.test_admin import TestAdmin as BaseTestAdmin +from openwisp_monitoring.device.tests.test_admin import ( + TestWifiSessionAdmin as BaseTestWifiSessionAdmin, +) from openwisp_monitoring.device.tests.test_api import TestDeviceApi as BaseTestDeviceApi from openwisp_monitoring.device.tests.test_apps import TestApps as BaseTestApps from openwisp_monitoring.device.tests.test_models import ( @@ -10,6 +13,9 @@ from openwisp_monitoring.device.tests.test_models import ( TestDeviceMonitoring as BaseTestDeviceMonitoring, ) +from openwisp_monitoring.device.tests.test_models import ( + TestWifiClientSession as BaseTestWifiClientSession, +) from openwisp_monitoring.device.tests.test_recovery import ( TestRecovery as BaseTestRecovery, ) @@ -57,6 +63,14 @@ class TestApps(BaseTestApps): pass +class TestWifiClientSession(BaseTestWifiClientSession): + pass + + +class TestWifiSessionAdmin(BaseTestWifiSessionAdmin): + pass + + # this is necessary to avoid excuting the base test suites del BaseTestRecovery del DeviceMonitoringTestCase @@ -67,3 +81,5 @@ class TestApps(BaseTestApps): del BaseTestDeviceApi del BaseTestAdmin del BaseTestApps +del BaseTestWifiClientSession +del BaseTestWifiSessionAdmin diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index 9e3a026ee..990ae5229 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -258,6 +258,8 @@ MONITORING_CHART_MODEL = 'sample_monitoring.Chart' MONITORING_METRIC_MODEL = 'sample_monitoring.Metric' MONITORING_ALERTSETTINGS_MODEL = 'sample_monitoring.AlertSettings' + DEVICE_MONITORING_WIFICLIENT_MODEL = 'sample_device_monitoring.WifiClient' + DEVICE_MONITORING_WIFISESSION_MODEL = 'sample_device_monitoring.WifiSession' DEVICE_MONITORING_DEVICEDATA_MODEL = 'sample_device_monitoring.DeviceData' DEVICE_MONITORING_DEVICEMONITORING_MODEL = ( 'sample_device_monitoring.DeviceMonitoring'