diff --git a/docs/user/checks.rst b/docs/user/checks.rst index a2ee51d8..dd94592a 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -62,3 +62,50 @@ parameters used for iperf3 checks (e.g. timing, port, username, password, may need to update the :doc:`metric configuration ` to enable alerts for the iperf3 check. + +.. _wifi_client_check: + +WiFi Client +----------- + +This check sends alerts based on the total number of WiFi Clients +connected to a device. It sends two types of alerts: + +- **Maximum WiFi Users**: When the total number of WiFi clients connected + to an access point exceeds a predetermined threshold. This functionality + provides valuable insights into the network's performance, signaling + when a specific access point is overwhelmed by an excessive number of + WiFi users. +- **Minimum WiFi Users**: When the total number of WiFi clients connected + to an access point remains at zero for a duration exceeding the + specified tolerance period. It serves as an indicator of whether the + access point is malfunctioning or if its placement is hindering user + connectivity. + +This check is **disabled by default**. To enable auto creation of this +check, set :ref:`openwisp_monitoring_auto_wifi_client_check` to ``True`` +and configure the task scheduling in your Django project: + +.. code-block:: python + + from datetime import timedelta + + OPENWISP_MONITORING_AUTO_WIFI_CLIENT_CHECK = True + CELERY_BEAT_SCHEDULE.update( + { + "run_wifi_client_checks": { + "task": "openwisp_monitoring.check.tasks.run_wifi_client_checks", + # Run check every 5 minutes + "schedule": timedelta(minutes=5), + "relative": True, + }, + } + ) + +You can also :doc:`add the WiFi Client check +` directly from the device page. + +You can use the +:ref:`openwisp_monitoring_wifi_client_check_snooze_schedule` setting to +disable this check on specific dates, such as during scheduled +maintenance, to avoid generating unnecessary alerts. diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 3fdf6380..9bbdd0f7 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -297,6 +297,60 @@ check when running on multiple servers. Make sure it is always greater than the total iperf3 check time, i.e. greater than the TCP + UDP test time. By default, it is set to **600 seconds (10 mins)**. +.. _openwisp_monitoring_auto_wifi_client_check: + +``OPENWISP_MONITORING_AUTO_WIFI_CLIENT_CHECK`` +---------------------------------------------- + +============ ========= +**type**: ``bool`` +**default**: ``False`` +============ ========= + +This setting allows you to choose whether :ref:`WiFi Client +` checks should be created automatically for newly +registered devices. It's disabled by default. + +.. _openwisp_monitoring_wifi_client_check_snooze_schedule: + +``OPENWISP_MONITORING_WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE`` +--------------------------------------------------------- + +============ ======== +**type**: ``list`` +**default**: ``[]`` +============ ======== + +This setting allows you to configure date ranges when the WiFi Client +check should not be executed. The date ranges should be in the format +``(start_date, end_date)`` where both dates are in the format ``MM-DD``. +Both start date and end date are inclusive. + +E.g.: + +.. code-block:: python + + OPENWISP_MONITORING_WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE = [ + # Date ranges can expand over months + ("12-24", "01-05"), + # Date ranges can be single day + ("01-26", "01-26"), + ] + +.. _openwisp_monitoring_wifi_client_check_interval: + +``OPENWISP_MONITORING_WIFI_CLIENT_CHECK_INTERVAL`` +-------------------------------------------------- + +============ ======= +**type**: ``int`` +**default**: ``5`` +============ ======= + +This setting allows you to configure the WiFi Client check interval used +by :ref:`WiFi Client checks `. By default it is set to +5 minutes. + .. _openwisp_monitoring_auto_charts: ``OPENWISP_MONITORING_AUTO_CHARTS`` diff --git a/openwisp_monitoring/check/apps.py b/openwisp_monitoring/check/apps.py index e0a45998..38145591 100644 --- a/openwisp_monitoring/check/apps.py +++ b/openwisp_monitoring/check/apps.py @@ -40,3 +40,12 @@ def _connect_signals(self): sender=load_model('config', 'Device'), dispatch_uid='auto_iperf3_check', ) + + if app_settings.AUTO_WIFI_CLIENT_CHECK: + from .base.models import auto_wifi_client_check_receiver + + post_save.connect( + auto_wifi_client_check_receiver, + sender=load_model('config', 'Device'), + dispatch_uid='auto_wifi_clients_check', + ) diff --git a/openwisp_monitoring/check/base/models.py b/openwisp_monitoring/check/base/models.py index 089717ad..562f12d7 100644 --- a/openwisp_monitoring/check/base/models.py +++ b/openwisp_monitoring/check/base/models.py @@ -13,6 +13,7 @@ auto_create_config_check, auto_create_iperf3_check, auto_create_ping, + auto_create_wifi_client_check, ) from openwisp_utils.base import TimeStampedEditableModel @@ -160,3 +161,21 @@ def auto_iperf3_check_receiver(sender, instance, created, **kwargs): object_id=str(instance.pk), ) ) + + +def auto_wifi_client_check_receiver(sender, instance, created, **kwargs): + """Implements OPENWISP_MONITORING_AUTO_WIFI_CLIENT_CHECK. + + The creation step is executed in the background. + """ + # we need to skip this otherwise this task will be executed + # every time the configuration is requested via checksum + if not created: + return + transaction_on_commit( + lambda: auto_create_wifi_client_check.delay( + model=sender.__name__.lower(), + app_label=sender._meta.app_label, + object_id=str(instance.pk), + ) + ) diff --git a/openwisp_monitoring/check/classes/__init__.py b/openwisp_monitoring/check/classes/__init__.py index a7d9fde2..3ad70518 100644 --- a/openwisp_monitoring/check/classes/__init__.py +++ b/openwisp_monitoring/check/classes/__init__.py @@ -1,3 +1,4 @@ from .config_applied import ConfigApplied # noqa from .iperf3 import Iperf3 # noqa from .ping import Ping # noqa +from .wifi_client import WifiClient # noqa diff --git a/openwisp_monitoring/check/classes/wifi_client.py b/openwisp_monitoring/check/classes/wifi_client.py new file mode 100644 index 00000000..c9ce6589 --- /dev/null +++ b/openwisp_monitoring/check/classes/wifi_client.py @@ -0,0 +1,52 @@ +from django.utils import timezone +from swapper import load_model + +from ...db import timeseries_db +from .. import settings as app_settings +from .base import BaseCheck + +AlertSettings = load_model('monitoring', 'AlertSettings') + + +class WifiClient(BaseCheck): + def check(self, store=True): + values = timeseries_db.read( + key='wifi_clients', + fields='COUNT(DISTINCT(clients))', + tags={ + 'content_type': self.related_object._meta.label_lower, + 'object_id': str(self.related_object.pk), + }, + since=int( + ( + timezone.localtime() + - timezone.timedelta( + minutes=app_settings.WIFI_CLIENT_CHECK_INTERVAL + ) + ).timestamp() + ), + ) + if not values: + result = 0 + else: + result = values[0]['count'] + if store: + self.store_result(result) + return result + + def store_result(self, result): + max_metric = self._get_metric('max_wifi_clients') + max_metric.write(result) + min_metric = self._get_metric('min_wifi_clients') + min_metric.write(result) + + def _get_metric(self, configuration): + metric, created = self._get_or_create_metric(configuration=configuration) + if created: + self._create_alert_setting(metric) + return metric + + def _create_alert_setting(self, metric): + alert_s = AlertSettings(metric=metric) + alert_s.full_clean() + alert_s.save() diff --git a/openwisp_monitoring/check/settings.py b/openwisp_monitoring/check/settings.py index 8f9db614..e8d62c90 100644 --- a/openwisp_monitoring/check/settings.py +++ b/openwisp_monitoring/check/settings.py @@ -8,6 +8,7 @@ ('openwisp_monitoring.check.classes.Ping', 'Ping'), ('openwisp_monitoring.check.classes.ConfigApplied', 'Configuration Applied'), ('openwisp_monitoring.check.classes.Iperf3', 'Iperf3'), + ('openwisp_monitoring.check.classes.WifiClient', 'Wifi Client'), ), ) AUTO_PING = get_settings_value('AUTO_PING', True) @@ -19,6 +20,13 @@ getattr(settings, 'OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY', True), ) PING_CHECK_CONFIG = get_settings_value('PING_CHECK_CONFIG', {}) +AUTO_WIFI_CLIENT_CHECK = get_settings_value('AUTO_WIFI_CLIENT_CHECK', False) +WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE = get_settings_value( + 'WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE', [] +) +WIFI_CLIENT_CHECK_INTERVAL = int( + get_settings_value('WIFI_CLIENT_CHECK_INTERVAL', 5) +) # in minutes AUTO_IPERF3 = get_settings_value('AUTO_IPERF3', False) IPERF3_CHECK_CONFIG = get_settings_value('IPERF3_CHECK_CONFIG', {}) IPERF3_CHECK_LOCK_EXPIRE = get_settings_value( diff --git a/openwisp_monitoring/check/tasks.py b/openwisp_monitoring/check/tasks.py index ee2c2b4a..cea3d485 100644 --- a/openwisp_monitoring/check/tasks.py +++ b/openwisp_monitoring/check/tasks.py @@ -5,11 +5,12 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.utils import timezone from swapper import load_model from openwisp_utils.tasks import OpenwispCeleryTask -from .settings import CHECKS_LIST +from .settings import CHECKS_LIST, WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE logger = logging.getLogger(__name__) @@ -52,6 +53,26 @@ def run_checks(checks=None): perform_check.delay(check['id']) +@shared_task(time_limit=2 * 60 * 60) +def run_wifi_client_checks(): + if WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE: + today = timezone.localdate() + # Format as MM-DD + today_month_day = today.strftime("%m-%d") + for start_date, end_date in WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE: + # Check if the date range wraps around the new year + if start_date <= end_date: + # Normal range within the same year + if start_date <= today_month_day <= end_date: + return + else: + # Wrap-around range spanning across years + if today_month_day >= start_date or today_month_day <= end_date: + return + + run_checks(checks=['openwisp_monitoring.check.classes.WifiClient']) + + @shared_task(time_limit=30 * 60) def perform_check(uuid): """Performs check with specified uuid. @@ -150,3 +171,32 @@ def auto_create_iperf3_check( ) check.full_clean() check.save() + + +@shared_task(base=OpenwispCeleryTask) +def auto_create_wifi_client_check( + model, app_label, object_id, check_model=None, content_type_model=None +): + """Implements the auto creation of the wifi_clients check. + + Called by the + openwisp_monitoring.check.models.auto_wifi_client_check_receiver. + """ + Check = check_model or get_check_model() + check_path = 'openwisp_monitoring.check.classes.WifiClient' + has_check = Check.objects.filter( + object_id=object_id, content_type__model='device', check_type=check_path + ).exists() + # create new check only if necessary + if has_check: + return + content_type_model = content_type_model or ContentType + ct = content_type_model.objects.get_by_natural_key(app_label=app_label, model=model) + check = Check( + name='Wifi Client', + check_type=check_path, + content_type=ct, + object_id=object_id, + ) + check.full_clean() + check.save() diff --git a/openwisp_monitoring/check/tests/__init__.py b/openwisp_monitoring/check/tests/__init__.py index ad5feee8..9a7a2b21 100644 --- a/openwisp_monitoring/check/tests/__init__.py +++ b/openwisp_monitoring/check/tests/__init__.py @@ -1,3 +1,10 @@ +from django.db.models.signals import post_save +from swapper import load_model + +from ..base.models import auto_wifi_client_check_receiver + +Device = load_model('config', 'Device') + _FPING_REACHABLE = ( '', bytes( @@ -10,3 +17,23 @@ '', bytes('192.168.255.255 : xmt/rcv/%loss = 3/0/100%', encoding='utf8'), ) + + +class AutoWifiClientCheck(object): + @classmethod + def setUpClass(cls): + super().setUpClass() + post_save.connect( + auto_wifi_client_check_receiver, + sender=Device, + dispatch_uid='auto_wifi_clients_check', + ) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + post_save.disconnect( + auto_wifi_client_check_receiver, + sender=Device, + dispatch_uid='auto_wifi_clients_check', + ) diff --git a/openwisp_monitoring/check/tests/test_models.py b/openwisp_monitoring/check/tests/test_models.py index b435df70..415da515 100644 --- a/openwisp_monitoring/check/tests/test_models.py +++ b/openwisp_monitoring/check/tests/test_models.py @@ -9,8 +9,14 @@ from ...device.tests import TestDeviceMonitoringMixin from .. import settings as app_settings -from ..classes import ConfigApplied, Iperf3, Ping -from ..tasks import auto_create_config_check, auto_create_iperf3_check, auto_create_ping +from ..classes import ConfigApplied, Iperf3, Ping, WifiClient +from ..tasks import ( + auto_create_config_check, + auto_create_iperf3_check, + auto_create_ping, + auto_create_wifi_client_check, +) +from . import AutoWifiClientCheck Check = load_model('check', 'Check') Metric = load_model('monitoring', 'Metric') @@ -19,10 +25,11 @@ Notification = load_model('openwisp_notifications', 'Notification') -class TestModels(TestDeviceMonitoringMixin, TransactionTestCase): +class TestModels(AutoWifiClientCheck, TestDeviceMonitoringMixin, TransactionTestCase): _PING = app_settings.CHECK_CLASSES[0][0] _CONFIG_APPLIED = app_settings.CHECK_CLASSES[1][0] _IPERF3 = app_settings.CHECK_CLASSES[2][0] + _WIFI_CLIENT = app_settings.CHECK_CLASSES[3][0] def test_check_str(self): c = Check(name='Test check') @@ -55,6 +62,12 @@ def test_check_class(self): check_type=self._IPERF3, ) self.assertEqual(c.check_class, Iperf3) + with self.subTest('Test WiFi Client check Class'): + c = Check( + name='WiFi Client class check', + check_type=self._WIFI_CLIENT, + ) + self.assertEqual(c.check_class, WifiClient) def test_base_check_class(self): path = 'openwisp_monitoring.check.classes.base.BaseCheck' @@ -101,6 +114,18 @@ def test_check_instance(self): self.assertEqual(i.related_object, obj) self.assertEqual(i.params, c.params) + with self.subTest('Test WiFi Client check instance'): + c = Check( + name='WiFi Client class check', + check_type=self._WIFI_CLIENT, + content_object=obj, + params={}, + ) + i = c.check_instance + self.assertIsInstance(i, WifiClient) + self.assertEqual(i.related_object, obj) + self.assertEqual(i.params, c.params) + def test_validation(self): with self.subTest('Test Ping check validation'): check = Check(name='Ping check', check_type=self._PING, params={}) @@ -124,7 +149,7 @@ def test_validation(self): def test_auto_check_creation(self): self.assertEqual(Check.objects.count(), 0) d = self._create_device(organization=self._create_org()) - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) with self.subTest('Test AUTO_PING'): c1 = Check.objects.filter(check_type=self._PING).first() self.assertEqual(c1.content_object, d) @@ -137,11 +162,15 @@ def test_auto_check_creation(self): c3 = Check.objects.filter(check_type=self._IPERF3).first() self.assertEqual(c3.content_object, d) self.assertEqual(self._IPERF3, c3.check_type) + with self.subTest('Test AUTO_WIFI_CLIENT_CHECK'): + c1 = Check.objects.filter(check_type=self._WIFI_CLIENT).first() + self.assertEqual(c1.content_object, d) + self.assertEqual(self._WIFI_CLIENT, c1.check_type) def test_device_deleted(self): self.assertEqual(Check.objects.count(), 0) d = self._create_device(organization=self._create_org()) - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) d.delete(check_deactivated=False) self.assertEqual(Check.objects.count(), 0) @@ -154,7 +183,7 @@ def test_config_modified_device_problem(self): self._create_config(status='modified', organization=self._create_org()) d = Device.objects.first() d.monitoring.update_status('ok') - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) self.assertEqual(Metric.objects.filter(object_id=d.id).count(), 0) self.assertEqual(AlertSettings.objects.count(), 0) check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() @@ -185,7 +214,7 @@ def test_config_error(self): self._create_config(status='error', organization=self._create_org()) dm = Device.objects.first().monitoring dm.update_status('ok') - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) self.assertEqual(Metric.objects.filter(object_id=dm.id).count(), 0) self.assertEqual(AlertSettings.objects.count(), 0) check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() @@ -220,7 +249,7 @@ def test_config_check_critical_metric(self): now() - timedelta(minutes=app_settings.CONFIG_CHECK_INTERVAL + 6) ): self._create_config(status='modified', organization=self._create_org()) - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) d = Device.objects.first() dm = d.monitoring dm.update_status('ok') @@ -239,7 +268,7 @@ def test_config_check_critical_metric(self): def test_no_duplicate_check_created(self): self._create_config(organization=self._create_org()) - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) d = Device.objects.first() auto_create_config_check.delay( model=Device.__name__.lower(), @@ -256,13 +285,18 @@ def test_no_duplicate_check_created(self): app_label=Device._meta.app_label, object_id=str(d.pk), ) - self.assertEqual(Check.objects.count(), 3) + auto_create_wifi_client_check.delay( + model=Device.__name__.lower(), + app_label=Device._meta.app_label, + object_id=str(d.pk), + ) + self.assertEqual(Check.objects.count(), 4) def test_device_unreachable_no_config_check(self): self._create_config(status='modified', organization=self._create_org()) d = self.device_model.objects.first() d.monitoring.update_status('critical') - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) c2 = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() c2.perform_check() self.assertEqual(Metric.objects.count(), 0) @@ -273,7 +307,7 @@ def test_device_unknown_no_config_check(self): self._create_config(status='modified', organization=self._create_org()) d = self.device_model.objects.first() d.monitoring.update_status('unknown') - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) c2 = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() c2.perform_check() self.assertEqual(Metric.objects.count(), 0) @@ -284,7 +318,7 @@ def test_device_organization_disabled_check_not_performed(self): self._create_config( status='modified', organization=self._create_org(is_active=False) ) - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() with patch(f'{self._CONFIG_APPLIED}.check') as mocked_check: check.perform_check() @@ -292,7 +326,7 @@ def test_device_organization_disabled_check_not_performed(self): def test_deactivated_device_check_not_performed(self): config = self._create_config(status='modified', organization=self._create_org()) - self.assertEqual(Check.objects.filter(is_active=True).count(), 3) + self.assertEqual(Check.objects.filter(is_active=True).count(), 4) config.device.deactivate() config.set_status_deactivated() check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() @@ -305,7 +339,7 @@ def test_config_check_problem_with_interval(self): d = self._create_device(organization=self._create_org()) self._create_config(device=d) check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() - self.assertEqual(Check.objects.count(), 3) + self.assertEqual(Check.objects.count(), 4) self.assertEqual(AlertSettings.objects.count(), 0) self.assertFalse(Metric.objects.filter(configuration='config_applied').exists()) d.monitoring.update_status('ok') diff --git a/openwisp_monitoring/check/tests/test_tasks.py b/openwisp_monitoring/check/tests/test_tasks.py new file mode 100644 index 00000000..a1595f03 --- /dev/null +++ b/openwisp_monitoring/check/tests/test_tasks.py @@ -0,0 +1,55 @@ +from unittest.mock import patch + +from django.test import TestCase +from freezegun import freeze_time + +from .. import settings as app_settings +from .. import tasks + + +class TestRunWifiClientChecks(TestCase): + _WIFI_CLIENT = app_settings.CHECK_CLASSES[3][0] + + @patch.object(tasks, 'run_checks') + def test_wifi_client_check_snooze_schedule_empty(self, mocked_run_checks): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_called_with(checks=[self._WIFI_CLIENT]) + + @patch.object( + tasks, + 'WIFI_CLIENT_CHECK_SNOOZE_SCHEDULE', + [ + ('01-26', '01-26'), + ('06-15', '08-31'), + ('12-25', '01-10'), + ], + ) + @patch.object(tasks, 'run_checks') + def test_wifi_client_check_snooze_schedule(self, mocked_run_checks, *args): + with freeze_time('2025-01-26'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_not_called() + + with freeze_time('2025-06-15'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_not_called() + + with freeze_time('2025-07-10'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_not_called() + + with freeze_time('2025-08-31'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_not_called() + + with freeze_time('2024-12-30'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_not_called() + + with freeze_time('2025-01-03'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_not_called() + + with freeze_time('2024-12-12'): + tasks.run_wifi_client_checks() + mocked_run_checks.assert_called_with(checks=[self._WIFI_CLIENT]) diff --git a/openwisp_monitoring/check/tests/test_wifi_client.py b/openwisp_monitoring/check/tests/test_wifi_client.py new file mode 100644 index 00000000..32691620 --- /dev/null +++ b/openwisp_monitoring/check/tests/test_wifi_client.py @@ -0,0 +1,70 @@ +from django.test import TransactionTestCase +from swapper import load_model + +from ...device.tests import TestDeviceMonitoringMixin +from .. import settings +from . import AutoWifiClientCheck + +Chart = load_model('monitoring', 'Chart') +AlertSettings = load_model('monitoring', 'AlertSettings') +Metric = load_model('monitoring', 'Metric') +Check = load_model('check', 'Check') +Device = load_model('config', 'Device') + + +class TestWifiClient( + AutoWifiClientCheck, + TestDeviceMonitoringMixin, + TransactionTestCase, +): + _WIFI_CLIENT = settings.CHECK_CLASSES[3][0] + + def test_store_result(self): + def _assert_wifi_client_metric(key): + metric = metric_qs.get(key=key) + self.assertEqual(metric.content_object, device) + points = self._read_metric(metric, limit=None) + self.assertEqual(len(points), 1) + self.assertEqual(points[0]['clients'], 3) + return metric + + device_data = self.create_test_data(no_resources=True, assertions=False) + device = Device.objects.get(id=device_data.id) + metric_qs = Metric.objects.filter( + key__in=['max_wifi_clients', 'min_wifi_clients'] + ) + alert_settings_qs = AlertSettings.objects.filter( + metric__key__in=['max_wifi_clients', 'min_wifi_clients'] + ) + # check created automatically by AUTO_WIFI_CLIENT_CHECK + self.assertEqual(Check.objects.count(), 4) + self.assertEqual(metric_qs.count(), 0) + self.assertEqual(alert_settings_qs.count(), 0) + check = Check.objects.filter(check_type=self._WIFI_CLIENT).first() + result = check.perform_check() + self.assertEqual(result, 3) + self.assertEqual(metric_qs.count(), 2) + self.assertEqual(alert_settings_qs.count(), 2) + + max_wifi_clients = _assert_wifi_client_metric('max_wifi_clients') + self.assertEqual(max_wifi_clients.alertsettings.operator, '>') + min_wifi_clients = _assert_wifi_client_metric('min_wifi_clients') + self.assertEqual(min_wifi_clients.alertsettings.operator, '<') + + def test_device_no_wifi_client(self): + device = self._create_device() + check = Check.objects.filter(check_type=self._WIFI_CLIENT).first() + result = check.perform_check() + self.assertEqual(result, 0) + max_wifi_client = Metric.objects.filter( + key='max_wifi_clients', object_id=device.id + ).first() + points = self._read_metric(max_wifi_client, limit=None) + self.assertEqual(len(points), 1) + self.assertEqual(points[0]['clients'], 0) + min_wifi_client = Metric.objects.filter( + key='min_wifi_clients', object_id=device.id + ).first() + points = self._read_metric(min_wifi_client, limit=None) + self.assertEqual(len(points), 1) + self.assertEqual(points[0]['clients'], 0) diff --git a/openwisp_monitoring/monitoring/configuration.py b/openwisp_monitoring/monitoring/configuration.py index 83b2e5a9..9764d015 100644 --- a/openwisp_monitoring/monitoring/configuration.py +++ b/openwisp_monitoring/monitoring/configuration.py @@ -283,6 +283,74 @@ def _get_access_tech(): } }, }, + 'max_wifi_clients': { + 'label': _('Max WiFi Clients'), + 'name': '{name}', + 'key': 'max_wifi_clients', + 'field_name': 'clients', + 'alert_settings': {'operator': '>', 'threshold': 40, 'tolerance': 60}, + 'notification': { + 'problem': { + 'verbose_name': 'Max WiFi clients PROBLEM', + 'verb': _('has more than'), + 'level': 'warning', + 'email_subject': _( + '[{site.name}] PROBLEM: Max WiFi Clients exceeded on {notification.target}' + ), + 'message': _( + 'The device [{notification.target}]({notification.target_link}) ' + '{notification.verb} {notification.actor.alertsettings.threshold} ' + 'WiFi clients connected.' + ), + }, + 'recovery': { + 'verbose_name': 'Max WiFi clients RECOVERY', + 'verb': _('has returned to normal levels'), + 'level': 'info', + 'email_subject': _( + '[{site.name}] RECOVERY: {notification.target} WiFi clients {notification.verb}' + ), + 'message': ( + 'The device [{notification.target}]({notification.target_link}) ' + 'WiFi clients {notification.verb}.' + ), + }, + }, + }, + 'min_wifi_clients': { + 'label': _('Min WiFi Clients'), + 'name': '{name}', + 'key': 'min_wifi_clients', + 'field_name': 'clients', + 'alert_settings': {'operator': '<', 'threshold': 0, 'tolerance': 240}, + 'notification': { + 'problem': { + 'verbose_name': 'Min WiFi clients PROBLEM', + 'verb': _('has less than'), + 'level': 'warning', + 'email_subject': _( + '[{site.name}] PROBLEM: {notification.target} {notification.verb} minimum WiFi clients' + ), + 'message': _( + 'The device [{notification.target}]({notification.target_link}) ' + '{notification.verb} {notification.actor.alertsettings.threshold} ' + 'WiFi clients connected.' + ), + }, + 'recovery': { + 'verbose_name': 'Min WiFi clients RECOVERY', + 'verb': _('has returned to normal levels'), + 'level': 'info', + 'email_subject': _( + '[{site.name}] RECOVERY: {notification.target} minimum WiFi clients {notification.verb}' + ), + 'message': ( + 'The device [{notification.target}]({notification.target_link}) ' + 'WiFi client {notification.verb}.' + ), + }, + }, + }, 'general_clients': { 'label': _('General WiFi Clients'), 'name': _('General WiFi Clients'), diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index f66a8e57..3b466205 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -190,6 +190,11 @@ ), 'relative': True, }, + 'run_wifi_client_checks': { + 'task': 'openwisp_monitoring.check.tasks.run_wifi_client_checks', + 'schedule': timedelta(minutes=5), + 'relative': True, + }, 'run_iperf3_checks': { 'task': 'openwisp_monitoring.check.tasks.run_checks', # https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html#crontab-schedules @@ -222,6 +227,8 @@ OPENWISP_MONITORING_API_BASEURL = 'http://testserver' # for testing AUTO_IPERF3 OPENWISP_MONITORING_AUTO_IPERF3 = True + # for testing AUTO_WIFI_CLIENT_CHECK + # OPENWISP_MONITORING_AUTO_WIFI_CLIENT_CHECK = True # Temporarily added to identify slow tests TEST_RUNNER = 'openwisp_utils.tests.TimeLoggingTestRunner'