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..4a7e76fe --- /dev/null +++ b/openwisp_monitoring/check/classes/wifi_client.py @@ -0,0 +1,43 @@ +from swapper import load_model + +from ...db import timeseries_db +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='now() - 5m', + ) + 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..e152fb52 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,10 @@ 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', [] +) 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/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..e64743fc 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