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 check for WiFi Clients #623

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 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
37 changes: 37 additions & 0 deletions docs/user/checks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,40 @@ parameters used for iperf3 checks (e.g. timing, port, username, password,
may need to update the :doc:`metric configuration
<device-checks-and-alert-settings>` to enable alerts for the iperf3
check.

.. _wifi_clients_check:

WiFi Clients
------------

This check sends alerts based on the total number of WiFi Clients
connected to a device. It sends two types of alerts:

- **Maximum WiFi Clients**: 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 clients.
- **Minimum WiFi Clients**: 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_clients_check` to ``True``
and configure the task scheduling in your Django project:

.. code-block:: python

from datetime import timedelta

OPENWISP_MONITORING_AUTO_WIFI_CLIENTS_CHECK = True

You can also :doc:`add the WiFi Clients check
<device-checks-and-alert-settings>` directly from the device page.

You can use the
:ref:`openwisp_monitoring_wifi_clients_check_snooze_schedule` setting to
disable this check on specific dates, such as during scheduled
maintenance, to avoid generating unnecessary alerts.
97 changes: 97 additions & 0 deletions docs/user/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,103 @@ 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_clients_check:

``OPENWISP_MONITORING_AUTO_WIFI_CLIENTS_CHECK``
-----------------------------------------------

============ =========
**type**: ``bool``
**default**: ``False``
============ =========

This setting allows you to choose whether :ref:`WiFi Clients
<wifi_clients_check>` checks should be created automatically for newly
registered devices. It's disabled by default.

.. _openwisp_monitoring_wifi_clients_check_snooze_schedule:

``OPENWISP_MONITORING_WIFI_CLIENTS_CHECK_SNOOZE_SCHEDULE``
----------------------------------------------------------

============ ========
**type**: ``list``
**default**: ``[]``
============ ========

This setting allows you to configure date-time ranges when the :ref:`WiFi
Clients <wifi_clients_check>` check should not be executed. The date-time
ranges should be in the format ``(start_datetime, end_datetime)`` where
both date-time are in the format ``MM-DD HH:MM`` (24 hours). Both start
date and end date are inclusive. You can omit the date or time part as
needed.

E.g.:

.. code-block:: python

OPENWISP_MONITORING_WIFI_CLIENTS_CHECK_SNOOZE_SCHEDULE = [
# Date ranges spanning across months
("12-24", "01-05"),
# Single-day exclusion
("01-26", "01-26"),
# Daily exclusion between specific times
("22:00", "06:00"),
# Specific date and time range exclusion
("08-15 12:00", "08-15 14:00"),
]

.. note::

**Date or Time Omission**:

- If you omit the date, the time range will be considered for
**every day**.
- If you omit the time, the exclusion will apply to the **entire
day**.

.. _openwisp_monitoring_wifi_clients_max_check_interval:

``OPENWISP_MONITORING_WIFI_CLIENTS_MAX_CHECK_INTERVAL``
-------------------------------------------------------

============ ==================
**type**: ``int``
**default**: ``5`` (in minutes)
============ ==================

Time period in minutes used by :ref:`WiFi Clients checks
<wifi_clients_check>` to monitor the maximum number of connected clients.

It checks for clients that have connected at least once between the
current time and the specified interval. For example, if the interval is
set to *5 minutes*, OpenWISP will look for clients that connected within
the last 5 minutes.

.. _openwisp_monitoring_wifi_clients_min_check_interval:

``OPENWISP_MONITORING_WIFI_CLIENTS_MIN_CHECK_INTERVAL``
-------------------------------------------------------

============ ============================
**type**: ``int``
**default**: ``4320`` (3 days in minutes)
============ ============================

Time period in minutes used by :ref:`WiFi Clients checks
<wifi_clients_check>` to monitor the minimum number of connected clients.

It checks for clients that have connected at least once between the
current time and the specified interval. For example, if the interval is
set to *4320 minutes (3 days)*, OpenWISP will look for clients that
connected within the last 3 days.

.. note::

The default value of this setting is intentionally set higher to avoid
false alerts that could occur when no devices are connected over
holidays (e.g., weekends).

.. _openwisp_monitoring_auto_charts:

``OPENWISP_MONITORING_AUTO_CHARTS``
Expand Down
10 changes: 10 additions & 0 deletions openwisp_monitoring/check/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.utils.translation import gettext_lazy as _
from swapper import load_model

from openwisp_monitoring.check import checks # noqa
from openwisp_monitoring.check import settings as app_settings


Expand Down Expand Up @@ -40,3 +41,12 @@ def _connect_signals(self):
sender=load_model('config', 'Device'),
dispatch_uid='auto_iperf3_check',
)

if app_settings.AUTO_WIFI_CLIENTS_CHECK:
from .base.models import auto_wifi_clients_check_receiver

post_save.connect(
auto_wifi_clients_check_receiver,
sender=load_model('config', 'Device'),
dispatch_uid='auto_wifi_clients_check',
)
19 changes: 19 additions & 0 deletions openwisp_monitoring/check/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
auto_create_config_check,
auto_create_iperf3_check,
auto_create_ping,
auto_create_wifi_clients_check,
)
from openwisp_utils.base import TimeStampedEditableModel

Expand Down Expand Up @@ -160,3 +161,21 @@ def auto_iperf3_check_receiver(sender, instance, created, **kwargs):
object_id=str(instance.pk),
)
)


def auto_wifi_clients_check_receiver(sender, instance, created, **kwargs):
"""Implements OPENWISP_MONITORING_AUTO_WIFI_CLIENTS_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_clients_check.delay(
model=sender.__name__.lower(),
app_label=sender._meta.app_label,
object_id=str(instance.pk),
)
)
76 changes: 76 additions & 0 deletions openwisp_monitoring/check/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from datetime import datetime

from django.core.checks import Error, register

from . import settings as app_settings


@register()
def check_wifi_clients_snooze_schedule(app_configs, **kwargs):
errors = []
setting_name = 'OPENWISP_MONITORING_WIFI_CLIENTS_CHECK_SNOOZE_SCHEDULE'
schedule = app_settings.WIFI_CLIENTS_CHECK_SNOOZE_SCHEDULE

if not isinstance(schedule, (list, tuple)):
errors.append(
Error(
'Invalid schedule format',
hint='Schedule must be a list of date-time ranges',
obj=setting_name,
)
)
return errors

for item in schedule:
if not isinstance(item, (list, tuple)) or len(item) != 2:
errors.append(
Error(
f'Invalid schedule entry format: {item}',
hint='Each schedule entry must be a pair of start and end times',
obj=setting_name,
)
)
continue

start, end = item
if not isinstance(start, str) or not isinstance(end, str):
errors.append(
Error(
f'Invalid time format: {item}',
hint='Use format "MM-DD HH:MM", "MM-DD", or "HH:MM"',
obj=setting_name,
)
)
continue

try:
# Check if both are time format (HH:MM)
if '-' not in start and '-' not in end:
datetime.strptime(start, '%H:%M')
datetime.strptime(end, '%H:%M')
# Check if both are date format (MM-DD)
elif ':' not in start and ':' not in end:
datetime.strptime(start, '%m-%d')
datetime.strptime(end, '%m-%d')
# Check if both are date-time format (MM-DD HH:MM)
elif len(start.split()) == 2 and len(end.split()) == 2:
datetime.strptime(start, '%m-%d %H:%M')
datetime.strptime(end, '%m-%d %H:%M')
else:
errors.append(
Error(
f'Inconsistent format: {item}',
hint='Both start and end must be in the same format (either both time or both date)',
obj=setting_name,
)
)
except ValueError:
errors.append(
Error(
f'Invalid date-time format: {item}',
hint='Use format "MM-DD HH:MM", "MM-DD", or "HH:MM"',
obj=setting_name,
)
)

return errors
1 change: 1 addition & 0 deletions openwisp_monitoring/check/classes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .config_applied import ConfigApplied # noqa
from .iperf3 import Iperf3 # noqa
from .ping import Ping # noqa
from .wifi_clients import WifiClients # noqa
12 changes: 12 additions & 0 deletions openwisp_monitoring/check/classes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ def validate(self):
def validate_params(self):
pass

@classmethod
def may_execute(cls):
"""
Class method that determines whether the check can be executed.

Returns:
bool: Always returns True by default.
Subclasses may override this method to implement
specific execution conditions.
"""
return True

def check(self, store=True):
raise NotImplementedError

Expand Down
Loading