From 0bf0bfc804cb8cabe7373854b2bc441ac9899a80 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 25 Apr 2024 15:57:29 +0530 Subject: [PATCH 01/42] [docs] Re-structured docs --- README.rst | 3135 ----------------- docs/developer/developer-docs.rst | 17 + docs/developer/exceptions.rst | 27 + docs/developer/extending.rst | 404 +++ docs/developer/installation.rst | 163 + docs/developer/management-commands.rst | 28 + docs/developer/monitoring-scripts.rst | 40 + .../registering-new-notification-types.rst | 10 + ...ring-unregistering-chart-configuration.rst | 73 + ...ing-unregistering-metric-configuration.rst | 169 + docs/overview.rst | 95 + docs/user/adaptive-size-charts.rst | 29 + docs/user/adding-checks-and-alertsettings.rst | 70 + docs/user/available-checks.rst | 49 + docs/user/dashboard-monitoring-charts.rst | 15 + .../user/default-alerts-and-notifications.rst | 17 + docs/user/default-metrics.rst | 267 ++ docs/user/device-health-status.rst | 35 + docs/user/iperf3-usage-instructions.rst | 284 ++ .../passive-vs-active-metric-collection.rst | 19 + docs/user/quickstart.rst | 136 + docs/user/rest-api.rst | 262 ++ docs/user/settings.rst | 749 ++++ docs/user/wifi-sessions.rst | 53 + 24 files changed, 3011 insertions(+), 3135 deletions(-) create mode 100644 docs/developer/developer-docs.rst create mode 100644 docs/developer/exceptions.rst create mode 100644 docs/developer/extending.rst create mode 100644 docs/developer/installation.rst create mode 100644 docs/developer/management-commands.rst create mode 100644 docs/developer/monitoring-scripts.rst create mode 100644 docs/developer/registering-new-notification-types.rst create mode 100644 docs/developer/registering-unregistering-chart-configuration.rst create mode 100644 docs/developer/registering-unregistering-metric-configuration.rst create mode 100644 docs/overview.rst create mode 100644 docs/user/adaptive-size-charts.rst create mode 100644 docs/user/adding-checks-and-alertsettings.rst create mode 100644 docs/user/available-checks.rst create mode 100644 docs/user/dashboard-monitoring-charts.rst create mode 100644 docs/user/default-alerts-and-notifications.rst create mode 100644 docs/user/default-metrics.rst create mode 100644 docs/user/device-health-status.rst create mode 100644 docs/user/iperf3-usage-instructions.rst create mode 100644 docs/user/passive-vs-active-metric-collection.rst create mode 100644 docs/user/quickstart.rst create mode 100644 docs/user/rest-api.rst create mode 100644 docs/user/settings.rst create mode 100644 docs/user/wifi-sessions.rst diff --git a/README.rst b/README.rst index 223c4b98..53967201 100644 --- a/README.rst +++ b/README.rst @@ -120,3141 +120,6 @@ Available Features ------------ -Installation instructions -------------------------- - -Deploy it in production -~~~~~~~~~~~~~~~~~~~~~~~ - -See: - -- `ansible-openwisp2 `_ -- `docker-openwisp `_ - -Install system dependencies -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -*openwisp-monitoring* uses InfluxDB to store metrics. Follow the -`installation instructions from InfluxDB's official documentation `_. - -**Note:** Only *InfluxDB 1.8.x* is supported in *openwisp-monitoring*. - -Install system packages: - -.. code-block:: shell - - sudo apt install -y openssl libssl-dev \ - gdal-bin libproj-dev libgeos-dev \ - fping - -Install stable version from PyPI -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install from PyPI: - -.. code-block:: shell - - pip install openwisp-monitoring - -Install development version -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install tarball: - -.. code-block:: shell - - pip install https://github.com/openwisp/openwisp-monitoring/tarball/master - -Alternatively, you can install via pip using git: - -.. code-block:: shell - - pip install -e git+git://github.com/openwisp/openwisp-monitoring#egg=openwisp_monitoring - -If you want to contribute, follow the instructions in -`"Installing for development" <#installing-for-development>`_ section. - -Installing for development -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install the system dependencies as mentioned in the -`"Install system dependencies" <#install-system-dependencies>`_ section. -Install these additional packages that are required for development: - -.. code-block:: shell - - sudo apt install -y sqlite3 libsqlite3-dev \ - libspatialite-dev libsqlite3-mod-spatialite \ - chromium - -Fork and clone the forked repository: - -.. code-block:: shell - - git clone git://github.com//openwisp-monitoring - -Navigate into the cloned repository: - -.. code-block:: shell - - cd openwisp-monitoring/ - -Start Redis and InfluxDB using Docker: - -.. code-block:: shell - - docker-compose up -d redis influxdb - -Setup and activate a virtual-environment. (we'll be using `virtualenv `_) - -.. code-block:: shell - - python -m virtualenv env - source env/bin/activate - -Make sure that you are using pip version 20.2.4 before moving to the next step: - -.. code-block:: shell - - pip install -U pip wheel setuptools - -Install development dependencies: - -.. code-block:: shell - - pip install -e . - pip install -r requirements-test.txt - npm install -g jshint stylelint - -Install WebDriver for Chromium for your browser version from ``_ -and extract ``chromedriver`` to one of directories from your ``$PATH`` (example: ``~/.local/bin/``). - -Create database: - -.. code-block:: shell - - cd tests/ - ./manage.py migrate - ./manage.py createsuperuser - -Run celery and celery-beat with the following commands (separate terminal windows are needed): - -.. code-block:: shell - - cd tests/ - celery -A openwisp2 worker -l info - celery -A openwisp2 beat -l info - -Launch development server: - -.. code-block:: shell - - ./manage.py runserver 0.0.0.0:8000 - -You can access the admin interface at http://127.0.0.1:8000/admin/. - -Run tests with: - -.. code-block:: shell - - ./runtests.py # using --parallel is not supported in this module - -Run quality assurance tests with: - -.. code-block:: shell - - ./run-qa-checks - -Install and run on docker -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Note**: This Docker image is for development purposes only. -For the official OpenWISP Docker images, see: `docker-openwisp -`_. - -Build from the Dockerfile: - -.. code-block:: shell - - docker-compose build - -Run the docker container: - -.. code-block:: shell - - docker-compose up - -Setup (integrate in an existing Django project) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Follow the setup instructions of `openwisp-controller -`_, then add the settings described below. - -.. code-block:: python - - INSTALLED_APPS = [ - # django apps - # all-auth - 'django.contrib.sites', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'django_extensions', - 'django_filters', - # openwisp2 modules - 'openwisp_users', - 'openwisp_controller.pki', - 'openwisp_controller.config', - 'openwisp_controller.connection', - 'openwisp_controller.geo', - # monitoring - 'openwisp_monitoring.monitoring', - 'openwisp_monitoring.device', - 'openwisp_monitoring.check', - 'nested_admin', - # notifications - 'openwisp_notifications', - # openwisp2 admin theme (must be loaded here) - 'openwisp_utils.admin_theme', - 'admin_auto_filters', - # admin - 'django.contrib.admin', - 'django.forms', - 'import_export' - # other dependencies ... - ] - - # Make sure you change them in production - # You can select one of the backends located in openwisp_monitoring.db.backends - TIMESERIES_DATABASE = { - 'BACKEND': 'openwisp_monitoring.db.backends.influxdb', - 'USER': 'openwisp', - 'PASSWORD': 'openwisp', - 'NAME': 'openwisp2', - 'HOST': 'localhost', - 'PORT': '8086', - 'OPTIONS': { - # Specify additional options to be used while initializing - # database connection. - # Note: These options may differ based on the backend used. - 'udp_writes': True, - 'udp_port': 8089, - } - } - -``urls.py``: - -.. code-block:: python - - from django.conf import settings - from django.conf.urls import include, url - from django.contrib.staticfiles.urls import staticfiles_urlpatterns - - from openwisp_utils.admin_theme.admin import admin, openwisp_admin - - openwisp_admin() - - urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), - url(r'', include('openwisp_controller.urls')), - url(r'', include('openwisp_monitoring.urls')), - ] - - urlpatterns += staticfiles_urlpatterns() - -Configure caching (you may use a different cache storage if you want): - -.. code-block:: python - - CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://localhost/0', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } - } - } - - SESSION_ENGINE = 'django.contrib.sessions.backends.cache' - SESSION_CACHE_ALIAS = 'default' - -Configure celery (you may use a different broker if you want): - -.. code-block:: python - - # here we show how to configure celery with redis but you can - # use other brokers if you want, consult the celery docs - CELERY_BROKER_URL = 'redis://localhost/1' - CELERY_BEAT_SCHEDULE = { - 'run_checks': { - 'task': 'openwisp_monitoring.check.tasks.run_checks', - # Executes only ping & config check every 5 min - 'schedule': timedelta(minutes=5), - 'args': ( - [ # Checks path - 'openwisp_monitoring.check.classes.Ping', - 'openwisp_monitoring.check.classes.ConfigApplied', - ], - ), - 'relative': True, - }, - # 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') - EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' - -If you decide to use Redis (as shown in these examples), -install the following python packages. - -.. code-block:: shell - - pip install redis django-redis - -Quickstart Guide ----------------- - -Install OpenWISP Monitoring -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install *OpenWISP Monitoring* using one of the methods mentioned in the -`"Installation instructions" <#installation-instructions>`_. - -Install openwisp-config on the device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -`Install the openwisp-config agent for OpenWrt -`_ -on your device. - -Install monitoring packages on the device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -`Install the openwrt-openwisp-monitoring packages -`_ -on your device. - -These packages collect and send the -monitoring data from the device to OpenWISP Monitoring and -are required to collect `metrics <#openwisp_monitoring_metrics>`_ -like interface traffic, WiFi clients, CPU load, memory usage, etc. - -**Note**: if you are an existing user of *openwisp-monitoring* and are using -the legacy *monitoring template* for collecting metrics, we highly recommend -`Migrating from monitoring scripts to monitoring packages -<#migrating-from-monitoring-scripts-to-monitoring-packages>`_. - -Make sure OpenWISP can reach your devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In order to perform `active checks <#available-checks>`_ and other actions like -`triggering the push of configuration changes -`_, -`executing shell commands -`_ or -`performing firmware upgrades -`_, -**the OpenWISP server needs to be able to reach the network devices**. - -There are mainly two deployment scenarios for OpenWISP: - -1. the OpenWISP server is deployed on the public internet and the devices are - geographically distributed across different locations: - **in this case a management tunnel is needed** -2. the OpenWISP server is deployed on a computer/server which is located in - the same Layer 2 network (that is, in the same LAN) where the devices - are located. - **in this case a management tunnel is NOT needed** - -1. Public internet deployment -############################# - -This is the most common scenario: - -- the OpenWISP server is deployed to the public internet, hence the - server has a public IPv4 (and IPv6) address and usually a valid - SSL certificate provided by Mozilla Letsencrypt or another SSL provider -- the network devices are geographically distributed across different - locations (different cities, different regions, different countries) - -In this scenario, the OpenWISP application will not be able to reach the -devices **unless a management tunnel** is used, for that reason having -a management VPN like OpenVPN, Wireguard or any other tunneling solution -is paramount, not only to allow OpenWISP to work properly, but also to -be able to perform debugging and troubleshooting when needed. - -In this scenario, the following requirements are needed: - -- a VPN server must be installed in a way that the OpenWISP - server can reach the VPN peers, for more information on how to do this - via OpenWISP please refer to the following sections: - - - `OpenVPN tunnel automation - `_ - - `Wireguard tunnel automation - `_ - - If you prefer to use other tunneling solutions (L2TP, Softether, etc.) - and know how to configure those solutions on your own, - that's totally fine as well. - - If the OpenWISP server is connected to a network infrastructure - which allows it to reach the devices via pre-existing tunneling or - Intranet solutions (eg: MPLS, SD-WAN), then setting up a VPN server - is not needed, as long as there's a dedicated interface on OpenWrt - which gets an IP address assigned to it and which is reachable from - the OpenWISP server. - -- The devices must be configured to join the management tunnel automatically, - either via a pre-existing configuration in the firmware or via an - `OpenWISP Template `_. - -- The `openwisp-config `_ - agent on the devices must be configured to specify - the ``management_interface`` option, the agent will communicate the - IP of the management interface to the OpenWISP Server and OpenWISP will - use the management IP for reaching the device. - - For example, if the *management interface* is named ``tun0``, - the openwisp-config configuration should look like the following example: - -.. code-block:: text - - # In /etc/config/openwisp on the device - - config controller 'http' - # ... other configuration directives ... - option management_interface 'tun0' - -2. LAN deployment -################# - -When the OpenWISP server and the network devices are deployed in the same -L2 network (eg: an office LAN) and the OpenWISP server is reachable -on the LAN address, OpenWISP can then use the **Last IP** field of the -devices to reach them. - -In this scenario it's necessary to set the -`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" <#openwisp-monitoring-management-ip-only>`_ -setting to ``False``. - -Creating checks for a device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, the `active checks <#available-checks>`_ are created -automatically for all devices, unless the automatic creation of some -specific checks has been disabled, for more information on how to do this, -refer to the `active checks <#available-checks>`_ section. - -These checks are created and executed in the background by celery workers. - -Passive vs Active Metric Collection ------------------------------------ - -The `the different device metric -`_ -collected by OpenWISP Monitoring can be divided in two categories: - -1. **metrics collected actively by OpenWISP**: - these metrics are collected by the celery workers running on the - OpenWISP server, which continuously sends network requests to the - devices and store the results; -2. **metrics collected passively by OpenWISP**: - these metrics are sent by the - `openwrt-openwisp-monitoring agent <#install-monitoring-packages-on-the-device>`_ - installed on the network devices and are collected by OpenWISP via - its REST API. - -The `"Available Checks" <#available-checks>`_ section of this document -lists the currently implemented **active checks**. - -Device Health Status --------------------- - -The possible values for the health status field (``DeviceMonitoring.status``) -are explained below. - -``UNKNOWN`` -~~~~~~~~~~~ - -Whenever a new device is created it will have ``UNKNOWN`` as it's default Heath Status. - -It implies that the system doesn't know whether the device is reachable yet. - -``OK`` -~~~~~~ - -Everything is working normally. - -``PROBLEM`` -~~~~~~~~~~~ - -One of the metrics has a value which is not in the expected range -(the threshold value set in the alert settings has been crossed). - -Example: CPU usage should be less than 90% but current value is at 95%. - -``CRITICAL`` -~~~~~~~~~~~~ - -One of the metrics defined in ``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` -has a value which is not in the expected range -(the threshold value set in the alert settings has been crossed). - -Example: ping is by default a critical metric which is expected to be always 1 -(reachable). - -Default Metrics ---------------- - -Device Status -~~~~~~~~~~~~~ - -This metric stores the status of the device for viewing purposes. - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-1.png - :align: center - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-2.png - :align: center - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-3.png - :align: center - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-4.png - :align: center - -Ping -~~~~ - -+--------------------+----------------------------------------------------------------+ -| **measurement**: | ``ping`` | -+--------------------+----------------------------------------------------------------+ -| **types**: | ``int`` (reachable and loss), ``float`` (rtt) | -+--------------------+----------------------------------------------------------------+ -| **fields**: | ``reachable``, ``loss``, ``rtt_min``, ``rtt_max``, ``rtt_avg`` | -+--------------------+----------------------------------------------------------------+ -| **configuration**: | ``ping`` | -+--------------------+----------------------------------------------------------------+ -| **charts**: | ``uptime``, ``packet_loss``, ``rtt`` | -+--------------------+----------------------------------------------------------------+ - -**Uptime**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/uptime.png - :align: center - -**Packet loss**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/packet-loss.png - :align: center - -**Round Trip Time**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/rtt.png - :align: center - -Traffic -~~~~~~~ - -+--------------------+--------------------------------------------------------------------------+ -| **measurement**: | ``traffic`` | -+--------------------+--------------------------------------------------------------------------+ -| **type**: | ``int`` | -+--------------------+--------------------------------------------------------------------------+ -| **fields**: | ``rx_bytes``, ``tx_bytes`` | -+--------------------+--------------------------------------------------------------------------+ -| **tags**: | .. code-block:: python | -| | | -| | { | -| | 'organization_id': '', | -| | 'ifname': '', | -| | # optional | -| | 'location_id': '', | -| | 'floorplan_id': '', | -| | } | -+--------------------+--------------------------------------------------------------------------+ -| **configuration**: | ``traffic`` | -+--------------------+--------------------------------------------------------------------------+ -| **charts**: | ``traffic`` | -+--------------------+--------------------------------------------------------------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/traffic.png - :align: center - -WiFi Clients -~~~~~~~~~~~~ - -+--------------------+--------------------------------------------------------------------------+ -| **measurement**: | ``wifi_clients`` | -+--------------------+--------------------------------------------------------------------------+ -| **type**: | ``int`` | -+--------------------+--------------------------------------------------------------------------+ -| **fields**: | ``clients`` | -+--------------------+--------------------------------------------------------------------------+ -| **tags**: | .. code-block:: python | -| | | -| | { | -| | 'organization_id': '', | -| | 'ifname': '', | -| | # optional | -| | 'location_id': '', | -| | 'floorplan_id': '', | -| | } | -+--------------------+--------------------------------------------------------------------------+ -| **configuration**: | ``clients`` | -+--------------------+--------------------------------------------------------------------------+ -| **charts**: | ``wifi_clients`` | -+--------------------+--------------------------------------------------------------------------+ - - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-clients.png - :align: center - -Memory Usage -~~~~~~~~~~~~ - -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **measurement**: | ```` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **type**: | ``float`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **fields**: | ``percent_used``, ``free_memory``, ``total_memory``, ``buffered_memory``, ``shared_memory``, ``cached_memory``, ``available_memory`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **configuration**: | ``memory`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **charts**: | ``memory`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/memory.png - :align: center - -CPU Load -~~~~~~~~ - -+--------------------+----------------------------------------------------+ -| **measurement**: | ``load`` | -+--------------------+----------------------------------------------------+ -| **type**: | ``float`` | -+--------------------+----------------------------------------------------+ -| **fields**: | ``cpu_usage``, ``load_1``, ``load_5``, ``load_15`` | -+--------------------+----------------------------------------------------+ -| **configuration**: | ``load`` | -+--------------------+----------------------------------------------------+ -| **charts**: | ``load`` | -+--------------------+----------------------------------------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/cpu-load.png - :align: center - -Disk Usage -~~~~~~~~~~ - -+--------------------+-------------------+ -| **measurement**: | ``disk`` | -+--------------------+-------------------+ -| **type**: | ``float`` | -+--------------------+-------------------+ -| **fields**: | ``used_disk`` | -+--------------------+-------------------+ -| **configuration**: | ``disk`` | -+--------------------+-------------------+ -| **charts**: | ``disk`` | -+--------------------+-------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/disk-usage.png - :align: center - -Mobile Signal Strength -~~~~~~~~~~~~~~~~~~~~~~ - -+--------------------+-----------------------------------------+ -| **measurement**: | ``signal_strength`` | -+--------------------+-----------------------------------------+ -| **type**: | ``float`` | -+--------------------+-----------------------------------------+ -| **fields**: | ``signal_strength``, ``signal_power`` | -+--------------------+-----------------------------------------+ -| **configuration**: | ``signal_strength`` | -+--------------------+-----------------------------------------+ -| **charts**: | ``signal_strength`` | -+--------------------+-----------------------------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-strength.png - :align: center - -Mobile Signal Quality -~~~~~~~~~~~~~~~~~~~~~~ - -+--------------------+-----------------------------------------+ -| **measurement**: | ``signal_quality`` | -+--------------------+-----------------------------------------+ -| **type**: | ``float`` | -+--------------------+-----------------------------------------+ -| **fields**: | ``signal_quality``, ``signal_quality`` | -+--------------------+-----------------------------------------+ -| **configuration**: | ``signal_quality`` | -+--------------------+-----------------------------------------+ -| **charts**: | ``signal_quality`` | -+--------------------+-----------------------------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-quality.png - :align: center - -Mobile Access Technology in use -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------------+-------------------+ -| **measurement**: | ``access_tech`` | -+--------------------+-------------------+ -| **type**: | ``int`` | -+--------------------+-------------------+ -| **fields**: | ``access_tech`` | -+--------------------+-------------------+ -| **configuration**: | ``access_tech`` | -+--------------------+-------------------+ -| **charts**: | ``access_tech`` | -+--------------------+-------------------+ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/access-technology.png - :align: center - -Iperf3 -~~~~~~ - -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **measurement**: | ``iperf3`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **types**: | | ``int`` (iperf3_result, sent_bytes_tcp, received_bytes_tcp, retransmits, sent_bytes_udp, total_packets, lost_packets), | -| | | ``float`` (sent_bps_tcp, received_bps_tcp, sent_bps_udp, jitter, lost_percent) | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **fields**: | | ``iperf3_result``, ``sent_bps_tcp``, ``received_bps_tcp``, ``sent_bytes_tcp``, ``received_bytes_tcp``, ``retransmits``, | -| | | ``sent_bps_udp``, ``sent_bytes_udp``, ``jitter``, ``total_packets``, ``lost_packets``, ``lost_percent`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **configuration**: | ``iperf3`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **charts**: | ``bandwidth``, ``transfer``, ``retransmits``, ``jitter``, ``datagram``, ``datagram_loss`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ - -**Bandwidth**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/bandwidth.png - :align: center - -**Transferred Data**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/transferred-data.png - :align: center - -**Retransmits**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/retransmits.png - :align: center - -**Jitter**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/jitter.png - :align: center - -**Datagram**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram.png - :align: center - -**Datagram loss**: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram-loss.png - :align: center - -For more info on how to configure and use Iperf3, please refer to -`iperf3 check usage instructions <#iperf3-check-usage-instructions>`_. - -**Note:** Iperf3 charts uses ``connect_points=True`` in -`default chart configuration <#openwisp_monitoring_charts>`_ that joins it's individual chart data points. - -Dashboard Monitoring Charts ---------------------------- - -.. figure:: https://github.com/openwisp/openwisp-monitoring/blob/docs/docs/1.1/dashboard-charts.png - :align: center - -OpenWISP Monitoring adds two timeseries charts to the admin dashboard: - -- **General WiFi clients Chart**: Shows the number of connected clients to the WiFi - interfaces of devices in the network. -- **General traffic Chart**: Shows the amount of traffic flowing in the network. - -You can configure the interfaces included in the **General traffic chart** using -the `"OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART" -<#openwisp_monitoring_dashboard_traffic_chart>`_ setting. - -Adaptive size charts --------------------- - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/adaptive-chart.png - :align: center - -When configuring charts, it is possible to flag their unit -as ``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `K`, `M`, `G` and `T` depending on -the size of each point, the summary values and Y axis are also resized. - -Example taken from the default configuration of the traffic chart: - -.. code-block:: python - - 'traffic': { - # other configurations for this chart - - # traffic measured in 'B' (bytes) - # unit B, KB, MB, GB, TB - 'unit': 'adaptive_prefix+B', - }, - - 'bandwidth': { - # adaptive unit for bandwidth related charts - # bandwidth measured in 'bps'(bits/sec) - # unit bps, Kbps, Mbps, Gbps, Tbps - 'unit': 'adaptive_prefix+bps', - }, - -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. - -You can also view open WiFi sessions of a device directly from the device's change admin -under the "WiFi Sessions" tab. - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-wifi-session-inline.png - :align: center - -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 ------------------------------- - -+-------------------------------+------------------------------------------------------------------+ -| Notification Type | Use | -+-------------------------------+------------------------------------------------------------------+ -| ``threshold_crossed`` | Fires when a metric crosses the boundary defined in the | -| | threshold value of the alert settings. | -+-------------------------------+------------------------------------------------------------------+ -| ``threshold_recovery`` | Fires when a metric goes back within the expected range. | -+-------------------------------+------------------------------------------------------------------+ -| ``connection_is_working`` | Fires when the connection to a device is working. | -+-------------------------------+------------------------------------------------------------------+ -| ``connection_is_not_working`` | Fires when the connection (eg: SSH) to a device stops working | -| | (eg: credentials are outdated, management IP address is | -| | outdated, or device is not reachable). | -+-------------------------------+------------------------------------------------------------------+ - -Available Checks ----------------- - -Ping -~~~~ - -This check returns information on device ``uptime`` and ``RTT (Round trip time)``. -The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. The ``fping`` -command is used to collect these metrics. -You may choose to disable auto creation of this check by setting -`OPENWISP_MONITORING_AUTO_PING <#OPENWISP_MONITORING_AUTO_PING>`_ to ``False``. - -You can change the default values used for ping checks using -`OPENWISP_MONITORING_PING_CHECK_CONFIG <#OPENWISP_MONITORING_PING_CHECK_CONFIG>`_ setting. - -Configuration applied -~~~~~~~~~~~~~~~~~~~~~ - -This check ensures that the `openwisp-config agent `_ -is running and applying configuration changes in a timely manner. -You may choose to disable auto creation of this check by using the -setting `OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK <#OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK>`_. - -This check runs periodically, but it is also triggered whenever the -configuration status of a device changes, this ensures the check reacts -quickly to events happening in the network and informs the user promptly -if there's anything that is not working as intended. - -Iperf3 -~~~~~~ - -This check provides network performance measurements such as maximum achievable bandwidth, -jitter, datagram loss etc of the device using `iperf3 utility `_. - -This check is **disabled by default**. You can enable auto creation of this check by setting the -`OPENWISP_MONITORING_AUTO_IPERF3 <#OPENWISP_MONITORING_AUTO_IPERF3>`_ to ``True``. - -You can also `add the iperf3 check -<#add-checks-and-alert-settings-from-the-device-page>`_ directly from the device page. - -It also supports tuning of various parameters. - -You can also change the parameters used for iperf3 checks (e.g. timing, port, username, -password, rsa_publc_key etc) using the `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ setting. - -**Note:** When setting `OPENWISP_MONITORING_AUTO_IPERF3 <#OPENWISP_MONITORING_AUTO_IPERF3>`_ to ``True``, -you may need to update the `metric configuration <#add-checks-and-alert-settings-from-the-device-page>`_ -to enable alerts for the iperf3 check. - -Iperf3 Check Usage Instructions -------------------------------- - -1. Make sure iperf3 is installed on the device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Register your device to OpenWISP and make sure the `iperf3 openwrt package -`_ is installed on the device, -eg: - -.. code-block:: shell - - opkg install iperf3 # if using without authentication - opkg install iperf3-ssl # if using with authentication (read below for more info) - -2. Ensure SSH access from OpenWISP is enabled on your devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Follow the steps in `"How to configure push updates" section of the -OpenWISP documentation -`_ -to allow SSH access to you device from OpenWISP. - -**Note:** Make sure device connection is enabled -& working with right update strategy i.e. ``OpenWRT SSH``. - -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png - :alt: Enable ssh access from openwisp to device - :align: center - -3. Set up and configure Iperf3 server settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -After having deployed your Iperf3 servers, you need to -configure the iperf3 settings on the django side of OpenWISP, -see the `test project settings for reference -`_. - -The host can be specified by hostname, IPv4 literal, or IPv6 literal. -Example: - -.. code-block:: python - - OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { - # 'org_pk' : {'host' : [], 'client_options' : {}} - 'a9734710-db30-46b0-a2fc-01f01046fe4f': { - # Some public iperf3 servers - # https://iperf.fr/iperf-servers.php#public-servers - 'host': ['iperf3.openwisp.io', '2001:db8::1', '192.168.5.2'], - 'client_options': { - 'port': 5209, - 'udp': {'bitrate': '30M'}, - 'tcp': {'bitrate': '0'}, - }, - }, - # another org - 'b9734710-db30-46b0-a2fc-01f01046fe4f': { - # available iperf3 servers - 'host': ['iperf3.openwisp2.io', '192.168.5.3'], - 'client_options': { - 'port': 5207, - 'udp': {'bitrate': '50M'}, - 'tcp': {'bitrate': '20M'}, - }, - }, - } - -**Note:** If an organization has more than one iperf3 server configured, then it enables -the iperf3 checks to run concurrently on different devices. If all of the available servers -are busy, then it will add the check back in the queue. - -The celery-beat configuration for the iperf3 check needs to be added too: - -.. code-block:: python - - from celery.schedules import crontab - - # Celery TIME_ZONE should be equal to django TIME_ZONE - # In order to schedule run_iperf3_checks on the correct time intervals - CELERY_TIMEZONE = TIME_ZONE - CELERY_BEAT_SCHEDULE = { - # Other celery beat configurations - # Celery beat configuration for iperf3 check - 'run_iperf3_checks': { - 'task': 'openwisp_monitoring.check.tasks.run_checks', - # https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html#crontab-schedules - # Executes check every 5 mins from 00:00 AM to 6:00 AM (night) - 'schedule': crontab(minute='*/5', hour='0-6'), - # Iperf3 check path - 'args': (['openwisp_monitoring.check.classes.Iperf3'],), - 'relative': True, - } - } - -Once the changes are saved, you will need to restart all the processes. - -**Note:** We recommended to configure this check to run in non peak -traffic times to not interfere with standard traffic. - -4. Run the check -~~~~~~~~~~~~~~~~ - -This should happen automatically if you have celery-beat correctly -configured and running in the background. -For testing purposes, you can run this check manually using the -`run_checks <#run_checks>`_ command. - -After that, you should see the iperf3 network measurements charts. - -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png - :alt: Iperf3 network measurement charts - -Iperf3 check parameters -~~~~~~~~~~~~~~~~~~~~~~~ - -Currently, iperf3 check supports the following parameters: - -+-----------------------+----------+--------------------------------------------------------------------+ -| **Parameter** | **Type** | **Default Value** | -+-----------------------+----------+--------------------------------------------------------------------+ -|``host`` | ``list`` | ``[]`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``username`` | ``str`` | ``''`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``password`` | ``str`` | ``''`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``rsa_public_key`` | ``str`` | ``''`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``client_options`` | +---------------------+----------+------------------------------------------+ | -| | | **Parameters** | **Type** | **Default Value** | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``port`` | ``int`` | ``5201`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``time`` | ``int`` | ``10`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``bytes`` | ``str`` | ``''`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``blockcount`` | ``str`` | ``''`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``window`` | ``str`` | ``0`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``parallel`` | ``int`` | ``1`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``reverse`` | ``bool`` | ``False`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``bidirectional`` | ``bool`` | ``False`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``connect_timeout`` | ``int`` | ``1000`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``tcp`` | +----------------+----------+---------------------+ | | -| | | | | **Parameters** | **Type** | **Default Value** | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``bitrate`` | ``str`` | ``0`` | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``length`` | ``str`` | ``128K`` | | | -| | | | +----------------+----------+---------------------+ | | -| | +---------------------+-----------------------------------------------------+ | -| | | ``udp`` | +----------------+----------+---------------------+ | | -| | | | | **Parameters** | **Type** | **Default Value** | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``bitrate`` | ``str`` | ``30M`` | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``length`` | ``str`` | ``0`` | | | -| | | | +----------------+----------+---------------------+ | | -| | +---------------------+-----------------------------------------------------+ | -+-----------------------+-------------------------------------------------------------------------------+ - -To learn how to use these parameters, please see the -`iperf3 check configuration example <#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_. - -Visit the `official documentation `_ -to learn more about the iperf3 parameters. - -Iperf3 authentication -~~~~~~~~~~~~~~~~~~~~~ - -By default iperf3 check runs without any kind of **authentication**, -in this section we will explain how to configure **RSA authentication** -between the **client** and the **server** to restrict connections -to authenticated clients. - -Server side -########### - -1. Generate RSA keypair -^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: shell - - openssl genrsa -des3 -out private.pem 2048 - openssl rsa -in private.pem -outform PEM -pubout -out public_key.pem - openssl rsa -in private.pem -out private_key.pem -outform PEM - -After running the commands mentioned above, the public key will be stored in -``public_key.pem`` which will be used in **rsa_public_key** parameter -in `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ -and the private key will be contained in the file ``private_key.pem`` -which will be used with **--rsa-private-key-path** command option when -starting the iperf3 server. - -2. Create user credentials -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: shell - - USER=iperfuser PASSWD=iperfpass - echo -n "{$USER}$PASSWD" | sha256sum | awk '{ print $1 }' - ---- - ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 #This is the hash of "iperfuser" - -Add the above hash with username in ``credentials.csv`` - -.. code-block:: shell - - # file format: username,sha256 - iperfuser,ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 - -3. Now start the iperf3 server with auth options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: shell - - iperf3 -s --rsa-private-key-path ./private_key.pem --authorized-users-path ./credentials.csv - -Client side (OpenWrt device) -############################ - -1. Install iperf3-ssl -^^^^^^^^^^^^^^^^^^^^^ - -Install the `iperf3-ssl openwrt package -`_ -instead of the normal -`iperf3 openwrt package `_ -because the latter comes without support for authentication. - -You may also check your installed **iperf3 openwrt package** features: - -.. code-block:: shell - - root@vm-openwrt:~ iperf3 -v - iperf 3.7 (cJSON 1.5.2) - Linux vm-openwrt 4.14.171 #0 SMP Thu Feb 27 21:05:12 2020 x86_64 - Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, - sendfile / zerocopy, socket pacing, authentication # contains 'authentication' - -2. Configure iperf3 check auth parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Now, add the following iperf3 authentication parameters -to `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ -in the settings: - -.. code-block:: python - - OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { - 'a9734710-db30-46b0-a2fc-01f01046fe4f': { - 'host': ['iperf1.openwisp.io', 'iperf2.openwisp.io', '192.168.5.2'], - # All three parameters (username, password, rsa_publc_key) - # are required for iperf3 authentication - 'username': 'iperfuser', - 'password': 'iperfpass', - # Add RSA public key without any headers - # ie. -----BEGIN PUBLIC KEY-----, -----BEGIN END KEY----- - 'rsa_public_key': ( - """ - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwuEm+iYrfSWJOupy6X3N - dxZvUCxvmoL3uoGAs0O0Y32unUQrwcTIxudy38JSuCccD+k2Rf8S4WuZSiTxaoea - 6Du99YQGVZeY67uJ21SWFqWU+w6ONUj3TrNNWoICN7BXGLE2BbSBz9YaXefE3aqw - GhEjQz364Itwm425vHn2MntSp0weWb4hUCjQUyyooRXPrFUGBOuY+VvAvMyAG4Uk - msapnWnBSxXt7Tbb++A5XbOMdM2mwNYDEtkD5ksC/x3EVBrI9FvENsH9+u/8J9Mf - 2oPl4MnlCMY86MQypkeUn7eVWfDnseNky7TyC0/IgCXve/iaydCCFdkjyo1MTAA4 - BQIDAQAB - """ - ), - 'client_options': { - 'port': 5209, - 'udp': {'bitrate': '20M'}, - 'tcp': {'bitrate': '0'}, - }, - } - } - -Adding Checks and Alert settings from the device page ------------------------------------------------------ - -We can add checks and define alert settings directly from the **device page**. - -To add a check, you just need to select an available **check type** as shown below: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-check.png - :align: center - -The following example shows how to use the -`OPENWISP_MONITORING_METRICS setting <#openwisp_monitoring_metrics>`_ -to reconfigure the system for `iperf3 check <#iperf3-1>`_ to send an alert if -the measured **TCP bandwidth** has been less than **10 Mbit/s** for more than **2 days**. - -1. By default, `Iperf3 checks <#iperf3-1>`_ come with default alert settings, -but it is easy to customize alert settings through the device page as shown below: - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-alertsettings.png - :align: center - -2. Now, add the following notification configuration to send an alert for **TCP bandwidth**: - -.. code-block:: python - - # Main project settings.py - from django.utils.translation import gettext_lazy as _ - - OPENWISP_MONITORING_METRICS = { - 'iperf3': { - 'notification': { - 'problem': { - 'verbose_name': 'Iperf3 PROBLEM', - 'verb': _('Iperf3 bandwidth is less than normal value'), - 'level': 'warning', - 'email_subject': _( - '[{site.name}] PROBLEM: {notification.target} {notification.verb}' - ), - 'message': _( - 'The device [{notification.target}]({notification.target_link}) ' - '{notification.verb}.' - ), - }, - 'recovery': { - 'verbose_name': 'Iperf3 RECOVERY', - 'verb': _('Iperf3 bandwidth now back to normal'), - 'level': 'info', - 'email_subject': _( - '[{site.name}] RECOVERY: {notification.target} {notification.verb}' - ), - 'message': _( - 'The device [{notification.target}]({notification.target_link}) ' - '{notification.verb}.' - ), - }, - }, - }, - } - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_warn.png - :align: center - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_info.png - :align: center - -**Note:** To access the features described above, the user must have permissions for ``Check`` and ``AlertSetting`` inlines, -these permissions are included by default in the "Administrator" and "Operator" groups and are shown in the screenshot below. - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/inline-permissions.png - :align: center - -Settings --------- - -``TIMESERIES_DATABASE`` -~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``str`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ - -.. code-block:: python - - TIMESERIES_DATABASE = { - 'BACKEND': 'openwisp_monitoring.db.backends.influxdb', - 'USER': 'openwisp', - 'PASSWORD': 'openwisp', - 'NAME': 'openwisp2', - 'HOST': 'localhost', - 'PORT': '8086', - 'OPTIONS': { - 'udp_writes': False, - 'udp_port': 8089, - } - } - -The following table describes all keys available in ``TIMESERIES_DATABASE`` -setting: - -+---------------+--------------------------------------------------------------------------------------+ -| **Key** | ``Description`` | -+---------------+--------------------------------------------------------------------------------------+ -| ``BACKEND`` | The timeseries database backend to use. You can select one of the backends | -| | located in ``openwisp_monitoring.db.backends`` | -+---------------+--------------------------------------------------------------------------------------+ -| ``USER`` | User for logging into the timeseries database | -+---------------+--------------------------------------------------------------------------------------+ -| ``PASSWORD`` | Password of the timeseries database user | -+---------------+--------------------------------------------------------------------------------------+ -| ``NAME`` | Name of the timeseries database | -+---------------+--------------------------------------------------------------------------------------+ -| ``HOST`` | IP address/hostname of machine where the timeseries database is running | -+---------------+--------------------------------------------------------------------------------------+ -| ``PORT`` | Port for connecting to the timeseries database | -+---------------+--------------------------------------------------------------------------------------+ -| ``OPTIONS`` | These settings depends on the timeseries backend: | -| | | -| | +-----------------+----------------------------------------------------------------+ | -| | | ``udp_writes`` | Whether to use UDP for writing data to the timeseries database | | -| | +-----------------+----------------------------------------------------------------+ | -| | | ``udp_port`` | Timeseries database port for writing data using UDP | | -| | +-----------------+----------------------------------------------------------------+ | -+---------------+--------------------------------------------------------------------------------------+ - -**Note:** UDP packets can have a maximum size of 64KB. When using UDP for writing timeseries -data, if the size of the data exceeds 64KB, TCP mode will be used instead. - -**Note:** If you want to use the ``openwisp_monitoring.db.backends.influxdb`` backend -with UDP writes enabled, then you need to enable two different ports for UDP -(each for different retention policy) in your InfluxDB configuration. The UDP configuration -section of your InfluxDB should look similar to the following: - -.. code-block:: text - - # For writing data with the "default" retention policy - [[udp]] - enabled = true - bind-address = "127.0.0.1:8089" - database = "openwisp2" - - # For writing data with the "short" retention policy - [[udp]] - enabled = true - bind-address = "127.0.0.1:8090" - database = "openwisp2" - retention-policy = 'short' - -If you are using `ansible-openwisp2 `_ -for deploying OpenWISP, you can set the ``influxdb_udp_mode`` ansible variable to ``true`` -in your playbook, this will make the ansible role automatically configure the InfluxDB UDP listeners. -You can refer to the `ansible-ow-influxdb's `_ -(a dependency of ansible-openwisp2) documentation to learn more. - -``OPENWISP_MONITORING_DEFAULT_RETENTION_POLICY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------+ -| **type**: | ``str`` | -+--------------+--------------------------+ -| **default**: | ``26280h0m0s`` (3 years) | -+--------------+--------------------------+ - -The default retention policy that applies to the timeseries data. - -``OPENWISP_MONITORING_SHORT_RETENTION_POLICY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``str`` | -+--------------+-------------+ -| **default**: | ``24h0m0s`` | -+--------------+-------------+ - -The default retention policy used to store raw device data. - -This data is only used to assess the recent status of devices, keeping -it for a long time would not add much benefit and would cost a lot more -in terms of disk space. - -``OPENWISP_MONITORING_AUTO_PING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -Whether ping checks are created automatically for devices. - -``OPENWISP_MONITORING_PING_CHECK_CONFIG`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ - -This setting allows to override the default ping check configuration defined in -``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``. - -For example, if you want to change only the **timeout** of -``ping`` you can use: - -.. code-block:: python - - OPENWISP_MONITORING_PING_CHECK_CONFIG = { - 'timeout': { - 'default': 1000, - }, - } - -If you are overriding the default value for any parameter -beyond the maximum or minimum value defined in -``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``, -you will also need to override the ``maximum`` or ``minimum`` fields -as following: - -.. code-block:: python - - OPENWISP_MONITORING_PING_CHECK_CONFIG = { - 'timeout': { - 'default': 2000, - 'minimum': 1500, - 'maximum': 2500, - }, - } - -**Note:** Above ``maximum`` and ``minimum`` values are only used for -validating custom parameters of a ``Check`` object. - -``OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -This setting allows you to choose whether `config_applied <#configuration-applied>`_ checks should be -created automatically for newly registered devices. It's enabled by default. - -``OPENWISP_MONITORING_CONFIG_CHECK_INTERVAL`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``5`` | -+--------------+-------------+ - -This setting allows you to configure the config check interval used by -`config_applied <#configuration-applied>`_. By default it is set to 5 minutes. - -``OPENWISP_MONITORING_AUTO_IPERF3`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``False`` | -+--------------+-------------+ - -This setting allows you to choose whether `iperf3 <#iperf3-1>`_ checks should be -created automatically for newly registered devices. It's disabled by default. - -``OPENWISP_MONITORING_IPERF3_CHECK_CONFIG`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ - -This setting allows to override the default iperf3 check configuration defined in -``openwisp_monitoring.check.classes.iperf3.DEFAULT_IPERF3_CHECK_CONFIG``. - -For example, you can change the values of `supported iperf3 check parameters <#iperf3-check-parameters>`_. - -.. code-block:: python - - OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { - # 'org_pk' : {'host' : [], 'client_options' : {}} - 'a9734710-db30-46b0-a2fc-01f01046fe4f': { - # Some public iperf3 servers - # https://iperf.fr/iperf-servers.php#public-servers - 'host': ['iperf3.openwisp.io', '2001:db8::1', '192.168.5.2'], - 'client_options': { - 'port': 6209, - # Number of parallel client streams to run - # note that iperf3 is single threaded - # so if you are CPU bound this will not - # yield higher throughput - 'parallel': 5, - # Set the connect_timeout (in milliseconds) for establishing - # the initial control connection to the server, the lower the value - # the faster the down iperf3 server will be detected (ex. 1000 ms (1 sec)) - 'connect_timeout': 1000, - # Window size / socket buffer size - 'window': '300K', - # Only one reverse condition can be chosen, - # reverse or bidirectional - 'reverse': True, - # Only one test end condition can be chosen, - # time, bytes or blockcount - 'blockcount': '1K', - 'udp': {'bitrate': '50M', 'length': '1460K'}, - 'tcp': {'bitrate': '20M', 'length': '256K'}, - }, - } - } - -``OPENWISP_MONITORING_IPERF3_CHECK_DELETE_RSA_KEY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------------------------+ -| **type**: | ``bool`` | -+--------------+-------------------------------+ -| **default**: | ``True`` | -+--------------+-------------------------------+ - -This setting allows you to set whether -`iperf3 check RSA public key <#configure-iperf3-check-for-authentication>`_ -will be deleted after successful completion of the check or not. - -``OPENWISP_MONITORING_IPERF3_CHECK_LOCK_EXPIRE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------------------------+ -| **type**: | ``int`` | -+--------------+-------------------------------+ -| **default**: | ``600`` | -+--------------+-------------------------------+ - -This setting allows you to set a cache lock expiration time for the iperf3 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_CHARTS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------------------------------------------------------------+ -| **type**: | ``list`` | -+--------------+-----------------------------------------------------------------+ -| **default**: | ``('traffic', 'wifi_clients', 'uptime', 'packet_loss', 'rtt')`` | -+--------------+-----------------------------------------------------------------+ - -Automatically created charts. - -``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------------------------------------------------------------+ -| **type**: | ``list`` of ``dict`` objects | -+--------------+-----------------------------------------------------------------+ -| **default**: | ``[{'key': 'ping', 'field_name': 'reachable'}]`` | -+--------------+-----------------------------------------------------------------+ - -Device metrics that are considered critical: - -when a value crosses the boundary defined in the "threshold value" field -of the alert settings related to one of these metric types, the health status -of the device related to the metric moves into ``CRITICAL``. - -By default, if devices are not reachable by pings they are flagged as ``CRITICAL``. - -``OPENWISP_MONITORING_HEALTH_STATUS_LABELS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------------------------------------------------+ -| **default**: | ``{'unknown': 'unknown', 'ok': 'ok', 'problem': 'problem', 'critical': 'critical'}`` | -+--------------+--------------------------------------------------------------------------------------+ - -This setting allows to change the health status labels, for example, if we -want to use ``online`` instead of ``ok`` and ``offline`` instead of ``critical``, -you can use the following configuration: - -.. code-block:: python - - OPENWISP_MONITORING_HEALTH_STATUS_LABELS = { - 'ok': 'online', - 'problem': 'problem', - '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`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -By default, only the management IP will be used to perform active checks to -the devices. - -If the devices are connecting to your OpenWISP instance using a shared layer2 -network, hence the OpenWSP server can reach the devices using the ``last_ip`` -field, you can set this to ``False``. - -**Note:** If this setting is not configured, it will fallback to the value of -`OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting -`_. -If ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, -then it will fallback to ``True``. - -``OPENWISP_MONITORING_DEVICE_RECOVERY_DETECTION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -When device recovery detection is enabled, recoveries are discovered as soon as -a device contacts the openwisp system again (eg: to get the configuration checksum -or to send monitoring metrics). - -This feature is enabled by default. - -If you use OpenVPN as the management VPN, you may want to check out a similar -integration built in **openwisp-network-topology**: when the status of an OpenVPN link -changes (detected by monitoring the status information of OpenVPN), the -network topology module will trigger the monitoring checks. -For more information see: -`Network Topology Device Integration `_ - -``OPENWISP_MONITORING_MAC_VENDOR_DETECTION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -Indicates whether mac addresses will be complemented with hardware vendor -information by performing lookups on the OUI -(Organization Unique Identifier) table. - -This feature is enabled by default. - -``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``dict`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ - -.. code-block:: python - - # default value of OPENWISP_MONITORING_RETRY_OPTIONS: - - dict( - max_retries=None, - retry_backoff=True, - retry_backoff_max=600, - retry_jitter=True, - ) - -Retry settings for recoverable failures during metric writes. - -By default if a metric write fails (eg: due to excessive load on timeseries database at that moment) -then the operation will be retried indefinitely with an exponential random backoff and a maximum delay of 10 minutes. - -This feature makes the monitoring system resilient to temporary outages and helps to prevent data loss. - -For more information regarding these settings, consult the `celery documentation -regarding automatic retries for known errors -`_. - -**Note:** The retry mechanism does not work when using ``UDP`` for writing -data to the timeseries database. It is due to the nature of ``UDP`` protocol -which does not acknowledge receipt of data packets. - -``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``dict`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ - -.. code-block:: python - - # default value of OPENWISP_MONITORING_RETRY_OPTIONS: - - dict( - max_retries=6, - delay=2 - ) - -On busy systems, communication with the timeseries DB can occasionally fail. -The timeseries DB backend will retry on any exception according to these settings. -The delay kicks in only after the third consecutive attempt. - -This setting shall not be confused with ``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS``, -which is used to configure the infinite retrying of the celery task which writes -metric data to the timeseries DB, while ``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` -deals with any other read/write operation on the timeseries DB which may fail. - -However these retries are not handled by celery but are simple python loops, -which will eventually give up if a problem persists. - -``OPENWISP_MONITORING_TIMESERIES_RETRY_DELAY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``2`` | -+--------------+-------------+ - -This settings allow you to configure the retry delay time (in seconds) after 3 failed attempt in timeseries database. - -This retry setting is used in retry mechanism to make the requests to the timeseries database resilient. - -This setting is independent of celery retry settings. - -``OPENWISP_MONITORING_DASHBOARD_MAP`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -Whether the geographic map in the dashboard is enabled or not. -This feature provides a geographic map which shows the locations -which have devices installed in and provides a visual representation -of the monitoring status of the devices, this allows to get -an overview of the network at glance. - -This feature is enabled by default and depends on the setting -``OPENWISP_ADMIN_DASHBOARD_ENABLED`` from -`openwisp-utils `__ -being set to ``True`` (which is the default). - -You can turn this off if you do not use the geographic features -of OpenWISP. - -``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------+ -| **default**: | ``{'__all__': ['wan', 'eth1', 'eth0.2']}`` | -+--------------+--------------------------------------------+ - -This settings allows to configure the interfaces which should -be included in the **General Traffic** chart in the admin dashboard. - -This setting should be defined in the following format: - -.. code-block::python - - OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART = { - '': [''] - } - -E.g., if you want the **General Traffic** chart to show data from -two interfaces for an organization, you need to configure this setting -as follows: - -.. code-block::python - - OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART = { - # organization uuid - 'f9601bbd-b6d5-4704-85e3-5851894437bf': ['eth1', 'eth2'] - } - -**Note**: The value of ``__all__`` key is used if an organization -does not have list of interfaces defined in ``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART``. - -**Note**: If a user can manage more than one organization (e.g. superusers), -then the **General Traffic** chart will always show data from interfaces -of ``__all__`` configuration. - -``OPENWISP_MONITORING_METRICS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ - -This setting allows to define additional metric configuration or to override -the default metric configuration defined in -``openwisp_monitoring.monitoring.configuration.DEFAULT_METRICS``. - -For example, if you want to change only the **field_name** of -``clients`` metric to ``wifi_clients`` you can use: - -.. code-block:: python - - from django.utils.translation import gettext_lazy as _ - - OPENWISP_MONITORING_METRICS = { - 'clients': { - 'label': _('WiFi clients'), - 'field_name': 'wifi_clients', - }, - } - -For example, if you want to change only the default alert settings of -``memory`` metric you can use: - -.. code-block:: python - - OPENWISP_MONITORING_METRICS = { - 'memory': { - 'alert_settings': {'threshold': 75, 'tolerance': 10} - }, - } - -For example, if you want to change only the notification of -``config_applied`` metric you can use: - -.. code-block:: python - - from django.utils.translation import gettext_lazy as _ - - OPENWISP_MONITORING_METRICS = { - 'config_applied': { - 'notification': { - 'problem': { - 'verbose_name': 'Configuration PROBLEM', - 'verb': _('has not been applied'), - 'email_subject': _( - '[{site.name}] PROBLEM: {notification.target} configuration ' - 'status issue' - ), - 'message': _( - 'The configuration for device [{notification.target}]' - '({notification.target_link}) {notification.verb} in a timely manner.' - ), - }, - 'recovery': { - 'verbose_name': 'Configuration RECOVERY', - 'verb': _('configuration has been applied again'), - 'email_subject': _( - '[{site.name}] RECOVERY: {notification.target} {notification.verb} ' - 'successfully' - ), - 'message': _( - 'The device [{notification.target}]({notification.target_link}) ' - '{notification.verb} successfully.' - ), - }, - }, - }, - } - -Or if you want to define a new metric configuration, which you can then -call in your custom code (eg: a custom check class), you can do so as follows: - -.. code-block:: python - - from django.utils.translation import gettext_lazy as _ - - OPENWISP_MONITORING_METRICS = { - 'top_fields_mean': { - 'name': 'Top Fields Mean', - 'key': '{key}', - 'field_name': '{field_name}', - 'label': '_(Top fields mean)', - 'related_fields': ['field1', 'field2', 'field3'], - }, - } - -``OPENWISP_MONITORING_CHARTS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ - -This setting allows to define additional charts or to override -the default chart configuration defined in -``openwisp_monitoring.monitoring.configuration.DEFAULT_CHARTS``. - -In the following example, we modify the description of the traffic chart: - -.. code-block:: python - - OPENWISP_MONITORING_CHARTS = { - 'traffic': { - 'description': ( - 'Network traffic, download and upload, measured on ' - 'the interface "{metric.key}", custom message here.' - ), - } - } - -Or if you want to define a new chart configuration, which you can then -call in your custom code (eg: a custom check class), you can do so as follows: - -.. code-block:: python - - from django.utils.translation import gettext_lazy as _ - - OPENWISP_MONITORING_CHARTS = { - 'ram': { - 'type': 'line', - 'title': 'RAM usage', - 'description': 'RAM usage', - 'unit': 'bytes', - 'order': 100, - 'query': { - 'influxdb': ( - "SELECT MEAN(total) AS total, MEAN(free) AS free, " - "MEAN(buffered) AS buffered FROM {key} WHERE time >= '{time}' AND " - "content_type = '{content_type}' AND object_id = '{object_id}' " - "GROUP BY time(1d)" - ) - }, - } - } - -In case you just want to change the colors used in a chart here's how to do it: - -.. code-block:: python - - OPENWISP_MONITORING_CHARTS = { - 'traffic': { - 'colors': ['#000000', '#cccccc', '#111111'] - } - } - -``OPENWISP_MONITORING_DEFAULT_CHART_TIME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+---------------------+---------------------------------------------+ -| **type**: | ``str`` | -+---------------------+---------------------------------------------+ -| **default**: | ``7d`` | -+---------------------+---------------------------------------------+ -| **possible values** | ``1d``, ``3d``, ``7d``, ``30d`` or ``365d`` | -+---------------------+---------------------------------------------+ - -Allows to set the default time period of the time series charts. - -``OPENWISP_MONITORING_AUTO_CLEAR_MANAGEMENT_IP`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -This setting allows you to automatically clear management_ip of a device -when it goes offline. It is enabled by default. - -``OPENWISP_MONITORING_API_URLCONF`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``string`` | -+--------------+-------------+ -| **default**: | ``None`` | -+--------------+-------------+ - -Changes the urlconf option of django urls to point the monitoring API -urls to another installed module, example, ``myapp.urls``. -(Useful when you have a seperate API instance.) - -``OPENWISP_MONITORING_API_BASEURL`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``string`` | -+--------------+-------------+ -| **default**: | ``None`` | -+--------------+-------------+ - -If you have a seperate server for API of openwisp-monitoring on a different -domain, you can use this option to change the base of the url, this will -enable you to point all the API urls to your openwisp-monitoring API server's -domain, example: ``https://mymonitoring.myapp.com``. - -``OPENWISP_MONITORING_CACHE_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------------+ -| **type**: | ``int`` | -+--------------+----------------------------------+ -| **default**: | ``86400`` (24 hours in seconds) | -+--------------+----------------------------------+ - -This setting allows to configure timeout (in seconds) for monitoring data cache. - -Registering / Unregistering Metric Configuration ------------------------------------------------- - -**OpenWISP Monitoring** provides registering and unregistering metric configuration through utility functions -``openwisp_monitoring.monitoring.configuration.register_metric`` and ``openwisp_monitoring.monitoring.configuration.unregister_metric``. -Using these functions you can register or unregister metric configurations from anywhere in your code. - -``register_metric`` -~~~~~~~~~~~~~~~~~~~ - -This function is used to register a new metric configuration from anywhere in your code. - -+--------------------------+------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------------+------------------------------------------------------+ -| **metric_name**: | A ``str`` defining name of the metric configuration. | -+--------------------------+------------------------------------------------------+ -|**metric_configuration**: | A ``dict`` defining configuration of the metric. | -+--------------------------+------------------------------------------------------+ - -An example usage has been shown below. - -.. code-block:: python - - from django.utils.translation import gettext_lazy as _ - from openwisp_monitoring.monitoring.configuration import register_metric - - # Define configuration of your metric - metric_config = { - 'label': _('Ping'), - 'name': 'Ping', - 'key': 'ping', - 'field_name': 'reachable', - 'related_fields': ['loss', 'rtt_min', 'rtt_max', 'rtt_avg'], - 'charts': { - 'uptime': { - 'type': 'bar', - 'title': _('Uptime'), - 'description': _( - 'A value of 100% means reachable, 0% means unreachable, values in ' - 'between 0% and 100% indicate the average reachability in the ' - 'period observed. Obtained with the fping linux program.' - ), - 'summary_labels': [_('Average uptime')], - 'unit': '%', - 'order': 200, - 'colorscale': { - 'max': 100, - 'min': 0, - 'label': _('Reachable'), - 'scale': [ - [[0, '#c13000'], - [0.1,'cb7222'], - [0.5,'#deed0e'], - [0.9, '#7db201'], - [1, '#498b26']], - ], - 'map': [ - [100, '#498b26', _('Reachable')], - [90, '#7db201', _('Mostly Reachable')], - [50, '#deed0e', _('Partly Reachable')], - [10, '#cb7222', _('Mostly Unreachable')], - [None, '#c13000', _('Unreachable')], - ], - 'fixed_value': 100, - }, - 'query': chart_query['uptime'], - }, - 'packet_loss': { - 'type': 'bar', - 'title': _('Packet loss'), - 'description': _( - 'Indicates the percentage of lost packets observed in ICMP probes. ' - 'Obtained with the fping linux program.' - ), - 'summary_labels': [_('Average packet loss')], - 'unit': '%', - 'colors': '#d62728', - 'order': 210, - 'query': chart_query['packet_loss'], - }, - 'rtt': { - 'type': 'scatter', - 'title': _('Round Trip Time'), - 'description': _( - 'Round trip time observed in ICMP probes, measuered in milliseconds.' - ), - 'summary_labels': [ - _('Average RTT'), - _('Average Max RTT'), - _('Average Min RTT'), - ], - 'unit': _(' ms'), - 'order': 220, - 'query': chart_query['rtt'], - }, - }, - 'alert_settings': {'operator': '<', 'threshold': 1, 'tolerance': 0}, - 'notification': { - 'problem': { - 'verbose_name': 'Ping PROBLEM', - 'verb': 'cannot be reached anymore', - 'level': 'warning', - 'email_subject': _( - '[{site.name}] {notification.target} is not reachable' - ), - 'message': _( - 'The device [{notification.target}] {notification.verb} anymore by our ping ' - 'messages.' - ), - }, - 'recovery': { - 'verbose_name': 'Ping RECOVERY', - 'verb': 'has become reachable', - 'level': 'info', - 'email_subject': _( - '[{site.name}] {notification.target} is reachable again' - ), - 'message': _( - 'The device [{notification.target}] {notification.verb} again by our ping ' - 'messages.' - ), - }, - }, - } - - # Register your custom metric configuration - register_metric('ping', metric_config) - -The above example will register one metric configuration (named ``ping``), three chart -configurations (named ``rtt``, ``packet_loss``, ``uptime``) as defined in the **charts** key, -two notification types (named ``ping_recovery``, ``ping_problem``) as defined in **notification** key. - -The ``AlertSettings`` of ``ping`` metric will by default use ``threshold`` and ``tolerance`` -defined in the ``alert_settings`` key. -You can always override them and define your own custom values via the *admin*. - -You can also use the ``alert_field`` key in metric configuration -which allows ``AlertSettings`` to check the ``threshold`` on -``alert_field`` instead of the default ``field_name`` key. - -**Note**: It will raise ``ImproperlyConfigured`` exception if a metric configuration -is already registered with same name (not to be confused with verbose_name). - -If you don't need to register a new metric but need to change a specific key of an -existing metric configuration, you can use `OPENWISP_MONITORING_METRICS <#openwisp_monitoring_metrics>`_. - -``unregister_metric`` -~~~~~~~~~~~~~~~~~~~~~ - -This function is used to unregister a metric configuration from anywhere in your code. - -+------------------+------------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+------------------------------------------------------+ -| **metric_name**: | A ``str`` defining name of the metric configuration. | -+------------------+------------------------------------------------------+ - -An example usage is shown below. - -.. code-block:: python - - from openwisp_monitoring.monitoring.configuration import unregister_metric - - # Unregister previously registered metric configuration - unregister_metric('metric_name') - -**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned metric -configuration is not registered. - -Registering / Unregistering Chart Configuration ------------------------------------------------ - -**OpenWISP Monitoring** provides registering and unregistering chart configuration through utility functions -``openwisp_monitoring.monitoring.configuration.register_chart`` and ``openwisp_monitoring.monitoring.configuration.unregister_chart``. -Using these functions you can register or unregister chart configurations from anywhere in your code. - -``register_chart`` -~~~~~~~~~~~~~~~~~~ - -This function is used to register a new chart configuration from anywhere in your code. - -+--------------------------+-----------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------------+-----------------------------------------------------+ -| **chart_name**: | A ``str`` defining name of the chart configuration. | -+--------------------------+-----------------------------------------------------+ -| **chart_configuration**: | A ``dict`` defining configuration of the chart. | -+--------------------------+-----------------------------------------------------+ - -An example usage has been shown below. - -.. code-block:: python - - from openwisp_monitoring.monitoring.configuration import register_chart - - # Define configuration of your chart - chart_config = { - 'type': 'histogram', - 'title': 'Histogram', - 'description': 'Histogram', - 'top_fields': 2, - 'order': 999, - 'query': { - 'influxdb': ( - "SELECT {fields|SUM|/ 1} FROM {key} " - "WHERE time >= '{time}' AND content_type = " - "'{content_type}' AND object_id = '{object_id}'" - ) - }, - } - - # Register your custom chart configuration - register_chart('chart_name', chart_config) - -**Note**: It will raise ``ImproperlyConfigured`` exception if a chart configuration -is already registered with same name (not to be confused with verbose_name). - -If you don't need to register a new chart but need to change a specific key of an -existing chart configuration, you can use `OPENWISP_MONITORING_CHARTS <#openwisp_monitoring_charts>`_. - -``unregister_chart`` -~~~~~~~~~~~~~~~~~~~~ - -This function is used to unregister a chart configuration from anywhere in your code. - -+------------------+-----------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+-----------------------------------------------------+ -| **chart_name**: | A ``str`` defining name of the chart configuration. | -+------------------+-----------------------------------------------------+ - -An example usage is shown below. - -.. code-block:: python - - from openwisp_monitoring.monitoring.configuration import unregister_chart - - # Unregister previously registered chart configuration - unregister_chart('chart_name') - -**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned chart -configuration is not registered. - -Registering new notification types ----------------------------------- - -You can define your own notification types using ``register_notification_type`` function from OpenWISP -Notifications. For more information, see the relevant `openwisp-notifications section about registering notification types -`_. - -Once a new notification type is registered, you have to use the `"notify" signal provided in -openwisp-notifications `_ -to send notifications for this type. - -Exceptions ----------- - -``TimeseriesWriteException`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` - -If there is any failure due while writing data in timeseries database, this exception shall -be raised with a helpful error message explaining the cause of the failure. -This exception will normally be caught and the failed write task will be retried in the background -so that there is no loss of data if failures occur due to overload of Timeseries server. -You can read more about this retry mechanism at `OPENWISP_MONITORING_WRITE_RETRY_OPTIONS <#openwisp-monitoring-write-retry-options>`_. - -``InvalidMetricConfigException`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` - -This exception shall be raised if the metric configuration is broken. - -``InvalidChartConfigException`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` - -This exception shall be raised if the chart configuration is broken. - -Rest API --------- - -Live documentation -~~~~~~~~~~~~~~~~~~ - -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png - -A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. - -Browsable web interface -~~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png - -Additionally, opening any of the endpoints `listed below <#list-of-endpoints>`_ -directly in the browser will show the `browsable API interface of Django-REST-Framework -`_, -which makes it even easier to find out the details of each endpoint. - -List of endpoints -~~~~~~~~~~~~~~~~~ - -Since the detailed explanation is contained in the `Live documentation <#live-documentation>`_ -and in the `Browsable web page <#browsable-web-interface>`_ of each point, -here we'll provide just a list of the available endpoints, -for further information please open the URL of the endpoint in your browser. - -Retrieve general monitoring charts -################################## - -.. code-block:: text - - GET /api/v1/monitoring/dashboard/ - -This API endpoint is used to show dashboard monitoring charts. It supports -multi-tenancy and allows filtering monitoring data by ``organization_slug``, -``location_id`` and ``floorplan_id`` e.g.: - -.. code-block:: text - - GET /api/v1/monitoring/dashboard/?organization_slug=,&location_id=,&floorplan_id=, - -- When retrieving chart data, the ``time`` parameter allows to specify - the time frame, eg: - - - ``1d``: returns data of the last day - - ``3d``: returns data of the last 3 days - - ``7d``: returns data of the last 7 days - - ``30d``: returns data of the last 30 days - - ``365d``: returns data of the last 365 days - -- In alternative to ``time`` it is possible to request chart data for a custom - date range by using the ``start`` and ``end`` parameters, eg: - -.. code-block:: text - - GET /api/v1/monitoring/dashboard/?start={start_datetime}&end={end_datetime} - -**Note**: ``start`` and ``end`` parameters should be in the format -``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. - -Retrieve device charts and device status data -############################################# - -.. code-block:: text - - GET /api/v1/monitoring/device/{pk}/?key={key}&status=true&time={timeframe} - -The format used for Device Status is inspired by -`NetJSON DeviceMonitoring `_. - -**Notes**: - -- If the request is made without ``?status=true`` the response will - contain only charts data and will not include any device status information - (current load average, ARP table, DCHP leases, etc.). - -- When retrieving chart data, the ``time`` parameter allows to specify - the time frame, eg: - - - ``1d``: returns data of the last day - - ``3d``: returns data of the last 3 days - - ``7d``: returns data of the last 7 days - - ``30d``: returns data of the last 30 days - - ``365d``: returns data of the last 365 days - -- In alternative to ``time`` it is possible to request chart data for a custom - date range by using the ``start`` and ``end`` parameters, eg: - -- The response contains device information, monitoring status (health status), - a list of metrics with their respective statuses, chart data and - device status information (only if ``?status=true``). - -- This endpoint can be accessed with session authentication, token authentication, - or alternatively with the device key passed as query string parameter - as shown below (`?key={key}`); - note: this method is meant to be used by the devices. - -.. code-block:: text - - GET /api/v1/monitoring/device/{pk}/?key={key}&status=true&start={start_datetime}&end={end_datetime} - -**Note**: ``start`` and ``end`` parameters must be in the format -``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. - -List device monitoring information -################################## - -.. code-block:: text - - GET /api/v1/monitoring/device/ - -**Notes**: - -- The response contains device information and monitoring status (health status), - but it does not include the information and - health status of the specific metrics, this information - can be retrieved in the detail endpoint of each device. - -- This endpoint can be accessed with session authentication and token authentication. - -**Available filters** - -Data can be filtered by health status (e.g. critical, ok, problem, and unknown) -to obtain the list of devices in the corresponding status, for example, -to retrieve the list of devices which are in critical conditions -(eg: unreachable), the following will work: - -.. code-block:: text - - GET /api/v1/monitoring/device/?monitoring__status=critical - -To filter a list of device monitoring data based -on their organization, you can use the ``organization_id``. - -.. code-block:: text - - GET /api/v1/monitoring/device/?organization={organization_id} - -To filter a list of device monitoring data based -on their organization slug, you can use the ``organization_slug``. - -.. code-block:: text - - GET /api/v1/monitoring/device/?organization_slug={organization_slug} - -Collect device metrics and status -################################# - -.. code-block:: text - - POST /api/v1/monitoring/device/{pk}/?key={key}&time={datetime} - -If data is latest then an additional parameter current can also be passed. For e.g.: - -.. code-block:: text - - POST /api/v1/monitoring/device/{pk}/?key={key}&time={datetime}¤t=true - -The format used for Device Status is inspired by -`NetJSON DeviceMonitoring `_. - -**Note**: the device data will be saved in the timeseries database using -the date time specified ``time``, this should be in the format -``%d-%m-%Y_%H:%M:%S.%f``, otherwise 400 Bad Response will be returned. - -If the request is made without passing the ``time`` argument, -the server local time will be used. - -The ``time`` parameter was added to support `resilient collection -and sending of data by the OpenWISP Monitoring Agent -`_, -this feature allows sending data collected while the device is offline. - -List nearby devices -################### - -.. code-block:: text - - GET /api/v1/monitoring/device/{pk}/nearby-devices/ - -Returns list of nearby devices along with respective distance (in metres) and -monitoring status. - -**Available filters** - -The list of nearby devices provides the following filters: - -- ``organization`` (Organization ID of the device) -- ``organization__slug`` (Organization slug of the device) -- ``monitoring__status`` (Monitoring status (``unknown``, ``ok``, ``problem``, or ``critical``)) -- ``model`` (Pipe `|` separated list of device models) -- ``distance__lte`` (Distance in metres) - -Here's a few examples: - -.. code-block:: text - - GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization={organization_id} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization__slug={organization_slug} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?monitoring__status={monitoring_status} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance} - -List wifi session -################# - -.. code-block:: text - - GET /api/v1/monitoring/wifi-session/ - -**Available filters** - -The list of wifi session provides the following filters: - -- ``device__organization`` (Organization ID of the device) -- ``device`` (Device ID) -- ``device__group`` (Device group ID) -- ``start_time`` (Start time of the wifi session) -- ``stop_time`` (Stop time of the wifi session) - -Here's a few examples: - -.. code-block:: text - - GET /api/v1/monitoring/wifi-session/?device__organization={organization_id} - GET /api/v1/monitoring/wifi-session/?device={device_id} - GET /api/v1/monitoring/wifi-session/?device__group={group_id} - GET /api/v1/monitoring/wifi-session/?start_time={stop_time} - GET /api/v1/monitoring/wifi-session/?stop_time={stop_time} - -**Note:** Both `start_time` and `stop_time` support -greater than or equal to, as well as less than or equal to, filter lookups. - -For example: - -.. code-block:: text - - GET /api/v1/monitoring/wifi-session/?start_time__gt={start_time} - GET /api/v1/monitoring/wifi-session/?start_time__gte={start_time} - GET /api/v1/monitoring/wifi-session/?stop_time__lt={stop_time} - GET /api/v1/monitoring/wifi-session/?stop_time__lte={stop_time} - -Get wifi session -################ - -.. code-block:: text - - GET /api/v1/monitoring/wifi-session/{id}/ - -Pagination -########## - -Wifi session endpoint support the ``page_size`` parameter -that allows paginating the results in conjunction with the page parameter. - -.. code-block:: text - - GET /api/v1/monitoring/wifi-session/?page_size=10 - GET /api/v1/monitoring/wifi-session/?page_size=10&page=1 - -Signals -------- - -``device_metrics_received`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.device.signals.device_metrics_received`` - -**Arguments**: - -- ``instance``: instance of ``Device`` whose metrics have been received -- ``request``: the HTTP request object -- ``time``: time with which metrics will be saved. If none, then server time will be used -- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues - -This signal is emitted when device metrics are received to the ``DeviceMetric`` -view (only when using HTTP POST). - -The signal is emitted just before a successful response is returned, -it is not sent if the response was not successful. - -``health_status_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.device.signals.health_status_changed`` - -**Arguments**: - -- ``instance``: instance of ``DeviceMonitoring`` whose status has been changed -- ``status``: the status by which DeviceMonitoring's existing status has been updated with - -This signal is emitted only if the health status of DeviceMonitoring object gets updated. - -``threshold_crossed`` -~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.monitoring.signals.threshold_crossed`` - -**Arguments**: - -- ``metric``: ``Metric`` object whose threshold defined in related alert settings was crossed -- ``alert_settings``: ``AlertSettings`` related to the ``Metric`` -- ``target``: related ``Device`` object -- ``first_time``: it will be set to true when the metric is written for the first time. It shall be set to false afterwards. -- ``tolerance_crossed``: it will be set to true if the metric has crossed the threshold for tolerance configured in alert settings. - Otherwise, it will be set to false. - -``first_time`` parameter can be used to avoid initiating unneeded actions. -For example, sending recovery notifications. - -This signal is emitted when the threshold value of a ``Metric`` defined in -alert settings is crossed. - -``pre_metric_write`` -~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.monitoring.signals.pre_metric_write`` - -**Arguments**: - -- ``metric``: ``Metric`` object whose data shall be stored in timeseries database -- ``values``: metric data that shall be stored in the timeseries database -- ``time``: time with which metrics will be saved -- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues - -This signal is emitted for every metric before the write operation is sent to -the timeseries database. - -``post_metric_write`` -~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_monitoring.monitoring.signals.post_metric_write`` - -**Arguments**: - -- ``metric``: ``Metric`` object whose data is being stored in timeseries database -- ``values``: metric data that is being stored in the timeseries database -- ``time``: time with which metrics will be saved -- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues - -This signal is emitted for every metric after the write operation is successfully -executed in the background. - -Management commands -------------------- - -``run_checks`` -~~~~~~~~~~~~~~ - -This command will execute all the `available checks <#available-checks>`_ for all the devices. -By default checks are run periodically by *celery beat*. You can learn more -about this in `Setup <#setup-integrate-in-an-existing-django-project>`_. - -Example usage: - -.. code-block:: shell - - cd tests/ - ./manage.py run_checks - -``migrate_timeseries`` -~~~~~~~~~~~~~~~~~~~~~~ - -This command triggers asynchronous migration of the time-series database. - -Example usage: - -.. code-block:: shell - - cd tests/ - ./manage.py migrate_timeseries - -Monitoring scripts ------------------- - -Monitoring scripts are now deprecated in favour of `monitoring packages `_. -Follow the migration guide in `Migrating from monitoring scripts to monitoring packages <#migrating-from-monitoring-scripts-to-monitoring-packages>`_ -section of this documentation. - -Migrating from monitoring scripts to monitoring packages --------------------------------------------------------- - -This section is intended for existing users of *openwisp-monitoring*. -The older version of *openwisp-monitoring* used *monitoring scripts* that -are now deprecated in favour of `monitoring packages `_. - -If you already had a *monitoring template* created on your installation, -then the migrations of *openwisp-monitoring* will update that template -by making the following changes: - -- The file name of all scripts will be appended with ``legacy-`` keyword - in order to differentiate them from the scripts bundled with the new packages. -- The ``/usr/sbin/legacy-openwisp-monitoring`` (previously ``/usr/sbin/openwisp-monitoring``) - script will be updated to exit if `openwisp-monitoring package `_ - is installed on the device. - -Install the `monitoring packages `_ -as mentioned in the `Install monitoring packages on device <#install-monitoring-packages-on-the-device>`_ -section of this documentation. - -After the proper configuration of the `openwisp-monitoring package `_ -on your device, you can remove the monitoring template from your devices. - -We suggest removing the monitoring template from the devices one at a time instead -of deleting the template. This ensures the correctness of -*openwisp monitoring package* configuration and you'll not miss out on -any monitoring data. - -**Note:** If you have made changes to the default monitoring template created -by *openwisp-monitoring* or you are using custom monitoring templates, then you should -remove such templates from the device before installing the -`monitoring packages `_. - -Extending openwisp-monitoring ------------------------------ - -One of the core values of the OpenWISP project is `Software Reusability `_, -for this reason *openwisp-monitoring* provides a set of base classes -which can be imported, extended and reused to create derivative apps. - -In order to implement your custom version of *openwisp-monitoring*, -you need to perform the steps described in the rest of this section. - -When in doubt, the code in the `test project `_ -and the ``sample apps`` namely `sample_check `_, -`sample_monitoring `_, `sample_device_monitoring `_ -will guide you in the correct direction: -just replicate and adapt that code to get a basic derivative of -*openwisp-monitoring* working. - -**Premise**: if you plan on using a customized version of this module, -we suggest to start with it since the beginning, because migrating your data -from the default module to your extended version may be time consuming. - -1. Initialize your custom module -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The first thing you need to do in order to extend any *openwisp-monitoring* app is create -a new django app which will contain your custom version of that *openwisp-monitoring* app. - -A django app is nothing more than a -`python package `_ -(a directory of python scripts), in the following examples we'll call these django apps as -``mycheck``, ``mydevicemonitoring``, ``mymonitoring`` but you can name it how you want:: - - django-admin startapp mycheck - django-admin startapp mydevicemonitoring - django-admin startapp mymonitoring - -Keep in mind that the command mentioned above must be called from a directory -which is available in your `PYTHON_PATH `_ -so that you can then import the result into your project. - -Now you need to add ``mycheck`` to ``INSTALLED_APPS`` in your ``settings.py``, -ensuring also that ``openwisp_monitoring.check`` has been removed: - -.. code-block:: python - - INSTALLED_APPS = [ - # ... other apps ... - # 'openwisp_monitoring.check', <-- comment out or delete this line - # 'openwisp_monitoring.device', <-- comment out or delete this line - # 'openwisp_monitoring.monitoring' <-- comment out or delete this line - 'mycheck', - 'mydevicemonitoring', - 'mymonitoring', - 'nested_admin', - ] - -For more information about how to work with django projects and django apps, -please refer to the `"Tutorial: Writing your first Django app" in the django docunmentation `_. - -2. Install ``openwisp-monitoring`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install (and add to the requirement of your project) *openwisp-monitoring*:: - - pip install --U https://github.com/openwisp/openwisp-monitoring/tarball/master - -3. Add ``EXTENDED_APPS`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -Add the following to your ``settings.py``: - -.. code-block:: python - - EXTENDED_APPS = ['device_monitoring', 'monitoring', 'check'] - -4. Add ``openwisp_utils.staticfiles.DependencyFinder`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add ``openwisp_utils.staticfiles.DependencyFinder`` to -``STATICFILES_FINDERS`` in your ``settings.py``: - -.. code-block:: python - - STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'openwisp_utils.staticfiles.DependencyFinder', - ] - -5. Add ``openwisp_utils.loaders.DependencyLoader`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: - -.. code-block:: python - - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'OPTIONS': { - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - 'openwisp_utils.loaders.DependencyLoader', - ], - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - } - ] - -6. Inherit the AppConfig class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Please refer to the following files in the sample app of the test project: - -- `sample_check/__init__.py `_. -- `sample_check/apps.py `_. -- `sample_monitoring/__init__.py `_. -- `sample_monitoring/apps.py `_. -- `sample_device_monitoring/__init__.py `_. -- `sample_device_monitoring/apps.py `_. - -For more information regarding the concept of ``AppConfig`` please refer to -the `"Applications" section in the django documentation `_. - -7. Create your custom models -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To extend ``check`` app, refer to `sample_check models.py file `_. - -To extend ``monitoring`` app, refer to `sample_monitoring models.py file `_. - -To extend ``device_monitoring`` app, refer to `sample_device_monitoring models.py file `_. - -**Note**: - -- For doubts regarding how to use, extend or develop models please refer to - the `"Models" section in the django documentation `_. -- For doubts regarding proxy models please refer to `proxy models `_. - -8. Add swapper configurations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add the following to your ``settings.py``: - -.. code-block:: python - - # Setting models for swapper module - # For extending check app - CHECK_CHECK_MODEL = 'YOUR_MODULE_NAME.Check' - # For extending monitoring app - MONITORING_CHART_MODEL = 'YOUR_MODULE_NAME.Chart' - MONITORING_METRIC_MODEL = 'YOUR_MODULE_NAME.Metric' - MONITORING_ALERTSETTINGS_MODEL = 'YOUR_MODULE_NAME.AlertSettings' - # 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``). - -9. Create database migrations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Create and apply database migrations:: - - ./manage.py makemigrations - ./manage.py migrate - -For more information, refer to the -`"Migrations" section in the django documentation `_. - -10. Create your custom admin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To extend ``check`` app, refer to `sample_check admin.py file `_. - -To extend ``monitoring`` app, refer to `sample_monitoring admin.py file `_. - -To extend ``device_monitoring`` app, refer to `sample_device_monitoring admin.py file `_. - -To introduce changes to the admin, you can do it in the two ways described below. - -**Note**: for doubts regarding how the django admin works, or how it can be customized, -please refer to `"The django admin site" section in the django documentation `_. - -1. Monkey patching -################## - -If the changes you need to add are relatively small, you can resort to monkey patching. - -For example, for ``check`` app you can do it as: - -.. code-block:: python - - from openwisp_monitoring.check.admin import CheckAdmin - - CheckAdmin.list_display.insert(1, 'my_custom_field') - CheckAdmin.ordering = ['-my_custom_field'] - -Similarly for ``device_monitoring`` app, you can do it as: - -.. code-block:: python - - 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: - -.. code-block:: python - - from openwisp_monitoring.monitoring.admin import MetricAdmin, AlertSettingsAdmin - - MetricAdmin.list_display.insert(1, 'my_custom_field') - MetricAdmin.ordering = ['-my_custom_field'] - AlertSettingsAdmin.list_display.insert(1, 'my_custom_field') - AlertSettingsAdmin.ordering = ['-my_custom_field'] - -2. Inheriting admin classes -########################### - -If you need to introduce significant changes and/or you don't want to resort to -monkey patching, you can proceed as follows: - -For ``check`` app, - -.. code-block:: python - - from django.contrib import admin - - from openwisp_monitoring.check.admin import CheckAdmin as BaseCheckAdmin - from swapper import load_model - - Check = load_model('check', 'Check') - - admin.site.unregister(Check) - - @admin.register(Check) - class CheckAdmin(BaseCheckAdmin): - # add your changes here - -For ``device_monitoring`` app, - -.. code-block:: python - - 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 - - from django.contrib import admin - - from openwisp_monitoring.monitoring.admin import ( - AlertSettingsAdmin as BaseAlertSettingsAdmin, - MetricAdmin as BaseMetricAdmin - ) - from swapper import load_model - - Metric = load_model('Metric') - AlertSettings = load_model('AlertSettings') - - admin.site.unregister(Metric) - admin.site.unregister(AlertSettings) - - @admin.register(Metric) - class MetricAdmin(BaseMetricAdmin): - # add your changes here - - @admin.register(AlertSettings) - class AlertSettingsAdmin(BaseAlertSettingsAdmin): - # add your changes here - -11. Create root URL configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Please refer to the `urls.py `_ -file in the test project. - -For more information about URL configuration in django, please refer to the -`"URL dispatcher" section in the django documentation `_. - -12. Create celery.py -~~~~~~~~~~~~~~~~~~~~ - -Please refer to the `celery.py `_ -file in the test project. - -For more information about the usage of celery in django, please refer to the -`"First steps with Django" section in the celery documentation `_. - -13. Import Celery Tasks -~~~~~~~~~~~~~~~~~~~~~~~ - -Add the following in your settings.py to import celery tasks from ``device_monitoring`` app. - -.. code-block:: python - - CELERY_IMPORTS = ('openwisp_monitoring.device.tasks',) - -14. Create the custom command ``run_checks`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Please refer to the `run_checks.py `_ -file in the test project. - -For more information about the usage of custom management commands in django, please refer to the -`"Writing custom django-admin commands" section in the django documentation `_. - -15. Import the automated tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When developing a custom application based on this module, it's a good idea -to import and run the base tests too, so that you can be sure the changes you're introducing -are not breaking some of the existing features of openwisp-monitoring. - -In case you need to add breaking changes, you can overwrite the tests defined -in the base classes to test your own behavior. - -For, extending ``check`` app see the `tests of sample_check app `_ -to find out how to do this. - -For, extending ``device_monitoring`` app see the `tests of sample_device_monitoring app `_ -to find out how to do this. - -For, extending ``monitoring`` app see the `tests of sample_monitoring app `_ -to find out how to do this. - -Other base classes that can be inherited and extended -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**The following steps are not required and are intended for more advanced customization.** - -``DeviceMetricView`` -#################### - -This view is responsible for displaying ``Charts`` and ``Status`` primarily. - -The full python path is: ``openwisp_monitoring.device.api.views.DeviceMetricView``. - -If you want to extend this view, you will have to perform the additional steps below. - -Step 1. Import and extend view: - -.. code-block:: python - - # mydevice/api/views.py - from openwisp_monitoring.device.api.views import ( - DeviceMetricView as BaseDeviceMetricView - ) - - class DeviceMetricView(BaseDeviceMetricView): - # add your customizations here ... - pass - -Step 2: remove the following line from your root ``urls.py`` file: - -.. code-block:: python - - re_path( - 'api/v1/monitoring/device/(?P[^/]+)/$', - views.device_metric, - name='api_device_metric', - ), - -Step 3: add an URL route pointing to your custom view in ``urls.py`` file: - -.. code-block:: python - - # urls.py - from mydevice.api.views import DeviceMetricView - - urlpatterns = [ - # ... other URLs - re_path(r'^(?P.*)$', DeviceMetricView.as_view(), name='api_device_metric',), - ] - Contributing ------------ diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst new file mode 100644 index 00000000..49045ed4 --- /dev/null +++ b/docs/developer/developer-docs.rst @@ -0,0 +1,17 @@ +Developers Documentation +------------------------ + +.. include:: /paritals/developers-docs-warning.rst + +.. toctree:: + :maxdepth: 1 + + ./installation.rst + ./management-commands.rst + ./monitoring-scripts.rst + ./registering-unregistering-metric-configuration.rst + ./registering-unregistering-chart-configuration.rst + ./registering-new-notification-types.rst + ./exceptions.rst + ./signals.rst + ./extending.rst diff --git a/docs/developer/exceptions.rst b/docs/developer/exceptions.rst new file mode 100644 index 00000000..bef66e52 --- /dev/null +++ b/docs/developer/exceptions.rst @@ -0,0 +1,27 @@ +Exceptions +---------- + +``TimeseriesWriteException`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` + +If there is any failure due while writing data in timeseries database, this exception shall +be raised with a helpful error message explaining the cause of the failure. +This exception will normally be caught and the failed write task will be retried in the background +so that there is no loss of data if failures occur due to overload of Timeseries server. +You can read more about this retry mechanism at `OPENWISP_MONITORING_WRITE_RETRY_OPTIONS <#openwisp-monitoring-write-retry-options>`_. + +``InvalidMetricConfigException`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` + +This exception shall be raised if the metric configuration is broken. + +``InvalidChartConfigException`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` + +This exception shall be raised if the chart configuration is broken. diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst new file mode 100644 index 00000000..17829bf9 --- /dev/null +++ b/docs/developer/extending.rst @@ -0,0 +1,404 @@ +Extending openwisp-monitoring +----------------------------- + +One of the core values of the OpenWISP project is `Software Reusability `_, +for this reason *openwisp-monitoring* provides a set of base classes +which can be imported, extended and reused to create derivative apps. + +In order to implement your custom version of *openwisp-monitoring*, +you need to perform the steps described in the rest of this section. + +When in doubt, the code in the `test project `_ +and the ``sample apps`` namely `sample_check `_, +`sample_monitoring `_, `sample_device_monitoring `_ +will guide you in the correct direction: +just replicate and adapt that code to get a basic derivative of +*openwisp-monitoring* working. + +**Premise**: if you plan on using a customized version of this module, +we suggest to start with it since the beginning, because migrating your data +from the default module to your extended version may be time consuming. + +1. Initialize your custom module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The first thing you need to do in order to extend any *openwisp-monitoring* app is create +a new django app which will contain your custom version of that *openwisp-monitoring* app. + +A django app is nothing more than a +`python package `_ +(a directory of python scripts), in the following examples we'll call these django apps as +``mycheck``, ``mydevicemonitoring``, ``mymonitoring`` but you can name it how you want:: + + django-admin startapp mycheck + django-admin startapp mydevicemonitoring + django-admin startapp mymonitoring + +Keep in mind that the command mentioned above must be called from a directory +which is available in your `PYTHON_PATH `_ +so that you can then import the result into your project. + +Now you need to add ``mycheck`` to ``INSTALLED_APPS`` in your ``settings.py``, +ensuring also that ``openwisp_monitoring.check`` has been removed: + +.. code-block:: python + + INSTALLED_APPS = [ + # ... other apps ... + # 'openwisp_monitoring.check', <-- comment out or delete this line + # 'openwisp_monitoring.device', <-- comment out or delete this line + # 'openwisp_monitoring.monitoring' <-- comment out or delete this line + 'mycheck', + 'mydevicemonitoring', + 'mymonitoring', + 'nested_admin', + ] + +For more information about how to work with django projects and django apps, +please refer to the `"Tutorial: Writing your first Django app" in the django docunmentation `_. + +2. Install ``openwisp-monitoring`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install (and add to the requirement of your project) *openwisp-monitoring*:: + + pip install --U https://github.com/openwisp/openwisp-monitoring/tarball/master + +3. Add ``EXTENDED_APPS`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Add the following to your ``settings.py``: + +.. code-block:: python + + EXTENDED_APPS = ['device_monitoring', 'monitoring', 'check'] + +4. Add ``openwisp_utils.staticfiles.DependencyFinder`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add ``openwisp_utils.staticfiles.DependencyFinder`` to +``STATICFILES_FINDERS`` in your ``settings.py``: + +.. code-block:: python + + STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'openwisp_utils.staticfiles.DependencyFinder', + ] + +5. Add ``openwisp_utils.loaders.DependencyLoader`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: + +.. code-block:: python + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'OPTIONS': { + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'openwisp_utils.loaders.DependencyLoader', + ], + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + } + ] + +6. Inherit the AppConfig class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please refer to the following files in the sample app of the test project: + +- `sample_check/__init__.py `_. +- `sample_check/apps.py `_. +- `sample_monitoring/__init__.py `_. +- `sample_monitoring/apps.py `_. +- `sample_device_monitoring/__init__.py `_. +- `sample_device_monitoring/apps.py `_. + +For more information regarding the concept of ``AppConfig`` please refer to +the `"Applications" section in the django documentation `_. + +7. Create your custom models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To extend ``check`` app, refer to `sample_check models.py file `_. + +To extend ``monitoring`` app, refer to `sample_monitoring models.py file `_. + +To extend ``device_monitoring`` app, refer to `sample_device_monitoring models.py file `_. + +**Note**: + +- For doubts regarding how to use, extend or develop models please refer to + the `"Models" section in the django documentation `_. +- For doubts regarding proxy models please refer to `proxy models `_. + +8. Add swapper configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add the following to your ``settings.py``: + +.. code-block:: python + + # Setting models for swapper module + # For extending check app + CHECK_CHECK_MODEL = 'YOUR_MODULE_NAME.Check' + # For extending monitoring app + MONITORING_CHART_MODEL = 'YOUR_MODULE_NAME.Chart' + MONITORING_METRIC_MODEL = 'YOUR_MODULE_NAME.Metric' + MONITORING_ALERTSETTINGS_MODEL = 'YOUR_MODULE_NAME.AlertSettings' + # 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``). + +9. Create database migrations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create and apply database migrations:: + + ./manage.py makemigrations + ./manage.py migrate + +For more information, refer to the +`"Migrations" section in the django documentation `_. + +10. Create your custom admin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To extend ``check`` app, refer to `sample_check admin.py file `_. + +To extend ``monitoring`` app, refer to `sample_monitoring admin.py file `_. + +To extend ``device_monitoring`` app, refer to `sample_device_monitoring admin.py file `_. + +To introduce changes to the admin, you can do it in the two ways described below. + +**Note**: for doubts regarding how the django admin works, or how it can be customized, +please refer to `"The django admin site" section in the django documentation `_. + +1. Monkey patching +################## + +If the changes you need to add are relatively small, you can resort to monkey patching. + +For example, for ``check`` app you can do it as: + +.. code-block:: python + + from openwisp_monitoring.check.admin import CheckAdmin + + CheckAdmin.list_display.insert(1, 'my_custom_field') + CheckAdmin.ordering = ['-my_custom_field'] + +Similarly for ``device_monitoring`` app, you can do it as: + +.. code-block:: python + + 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: + +.. code-block:: python + + from openwisp_monitoring.monitoring.admin import MetricAdmin, AlertSettingsAdmin + + MetricAdmin.list_display.insert(1, 'my_custom_field') + MetricAdmin.ordering = ['-my_custom_field'] + AlertSettingsAdmin.list_display.insert(1, 'my_custom_field') + AlertSettingsAdmin.ordering = ['-my_custom_field'] + +2. Inheriting admin classes +########################### + +If you need to introduce significant changes and/or you don't want to resort to +monkey patching, you can proceed as follows: + +For ``check`` app, + +.. code-block:: python + + from django.contrib import admin + + from openwisp_monitoring.check.admin import CheckAdmin as BaseCheckAdmin + from swapper import load_model + + Check = load_model('check', 'Check') + + admin.site.unregister(Check) + + @admin.register(Check) + class CheckAdmin(BaseCheckAdmin): + # add your changes here + +For ``device_monitoring`` app, + +.. code-block:: python + + 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 + + from django.contrib import admin + + from openwisp_monitoring.monitoring.admin import ( + AlertSettingsAdmin as BaseAlertSettingsAdmin, + MetricAdmin as BaseMetricAdmin + ) + from swapper import load_model + + Metric = load_model('Metric') + AlertSettings = load_model('AlertSettings') + + admin.site.unregister(Metric) + admin.site.unregister(AlertSettings) + + @admin.register(Metric) + class MetricAdmin(BaseMetricAdmin): + # add your changes here + + @admin.register(AlertSettings) + class AlertSettingsAdmin(BaseAlertSettingsAdmin): + # add your changes here + +11. Create root URL configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please refer to the `urls.py `_ +file in the test project. + +For more information about URL configuration in django, please refer to the +`"URL dispatcher" section in the django documentation `_. + +12. Create celery.py +~~~~~~~~~~~~~~~~~~~~ + +Please refer to the `celery.py `_ +file in the test project. + +For more information about the usage of celery in django, please refer to the +`"First steps with Django" section in the celery documentation `_. + +13. Import Celery Tasks +~~~~~~~~~~~~~~~~~~~~~~~ + +Add the following in your settings.py to import celery tasks from ``device_monitoring`` app. + +.. code-block:: python + + CELERY_IMPORTS = ('openwisp_monitoring.device.tasks',) + +14. Create the custom command ``run_checks`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please refer to the `run_checks.py `_ +file in the test project. + +For more information about the usage of custom management commands in django, please refer to the +`"Writing custom django-admin commands" section in the django documentation `_. + +15. Import the automated tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When developing a custom application based on this module, it's a good idea +to import and run the base tests too, so that you can be sure the changes you're introducing +are not breaking some of the existing features of openwisp-monitoring. + +In case you need to add breaking changes, you can overwrite the tests defined +in the base classes to test your own behavior. + +For, extending ``check`` app see the `tests of sample_check app `_ +to find out how to do this. + +For, extending ``device_monitoring`` app see the `tests of sample_device_monitoring app `_ +to find out how to do this. + +For, extending ``monitoring`` app see the `tests of sample_monitoring app `_ +to find out how to do this. + +Other base classes that can be inherited and extended +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**The following steps are not required and are intended for more advanced customization.** + +``DeviceMetricView`` +#################### + +This view is responsible for displaying ``Charts`` and ``Status`` primarily. + +The full python path is: ``openwisp_monitoring.device.api.views.DeviceMetricView``. + +If you want to extend this view, you will have to perform the additional steps below. + +Step 1. Import and extend view: + +.. code-block:: python + + # mydevice/api/views.py + from openwisp_monitoring.device.api.views import ( + DeviceMetricView as BaseDeviceMetricView + ) + + class DeviceMetricView(BaseDeviceMetricView): + # add your customizations here ... + pass + +Step 2: remove the following line from your root ``urls.py`` file: + +.. code-block:: python + + re_path( + 'api/v1/monitoring/device/(?P[^/]+)/$', + views.device_metric, + name='api_device_metric', + ), + +Step 3: add an URL route pointing to your custom view in ``urls.py`` file: + +.. code-block:: python + + # urls.py + from mydevice.api.views import DeviceMetricView + + urlpatterns = [ + # ... other URLs + re_path(r'^(?P.*)$', DeviceMetricView.as_view(), name='api_device_metric',), + ] diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst new file mode 100644 index 00000000..8fb98d13 --- /dev/null +++ b/docs/developer/installation.rst @@ -0,0 +1,163 @@ +Installation instructions +------------------------- + +Deploy it in production +~~~~~~~~~~~~~~~~~~~~~~~ + +See: + +- `ansible-openwisp2 `_ +- `docker-openwisp `_ + +Install system dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*openwisp-monitoring* uses InfluxDB to store metrics. Follow the +`installation instructions from InfluxDB's official documentation `_. + +**Note:** Only *InfluxDB 1.8.x* is supported in *openwisp-monitoring*. + +Install system packages: + +.. code-block:: shell + + sudo apt install -y openssl libssl-dev \ + gdal-bin libproj-dev libgeos-dev \ + fping + +Install stable version from PyPI +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install from PyPI: + +.. code-block:: shell + + pip install openwisp-monitoring + +Install development version +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install tarball: + +.. code-block:: shell + + pip install https://github.com/openwisp/openwisp-monitoring/tarball/master + +Alternatively, you can install via pip using git: + +.. code-block:: shell + + pip install -e git+git://github.com/openwisp/openwisp-monitoring#egg=openwisp_monitoring + +If you want to contribute, follow the instructions in +`"Installing for development" <#installing-for-development>`_ section. + +Installing for development +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install the system dependencies as mentioned in the +`"Install system dependencies" <#install-system-dependencies>`_ section. +Install these additional packages that are required for development: + +.. code-block:: shell + + sudo apt install -y sqlite3 libsqlite3-dev \ + libspatialite-dev libsqlite3-mod-spatialite \ + chromium + +Fork and clone the forked repository: + +.. code-block:: shell + + git clone git://github.com//openwisp-monitoring + +Navigate into the cloned repository: + +.. code-block:: shell + + cd openwisp-monitoring/ + +Start Redis and InfluxDB using Docker: + +.. code-block:: shell + + docker-compose up -d redis influxdb + +Setup and activate a virtual-environment. (we'll be using `virtualenv `_) + +.. code-block:: shell + + python -m virtualenv env + source env/bin/activate + +Make sure that you are using pip version 20.2.4 before moving to the next step: + +.. code-block:: shell + + pip install -U pip wheel setuptools + +Install development dependencies: + +.. code-block:: shell + + pip install -e . + pip install -r requirements-test.txt + npm install -g jshint stylelint + +Install WebDriver for Chromium for your browser version from ``_ +and extract ``chromedriver`` to one of directories from your ``$PATH`` (example: ``~/.local/bin/``). + +Create database: + +.. code-block:: shell + + cd tests/ + ./manage.py migrate + ./manage.py createsuperuser + +Run celery and celery-beat with the following commands (separate terminal windows are needed): + +.. code-block:: shell + + cd tests/ + celery -A openwisp2 worker -l info + celery -A openwisp2 beat -l info + +Launch development server: + +.. code-block:: shell + + ./manage.py runserver 0.0.0.0:8000 + +You can access the admin interface at http://127.0.0.1:8000/admin/. + +Run tests with: + +.. code-block:: shell + + ./runtests.py # using --parallel is not supported in this module + +Run quality assurance tests with: + +.. code-block:: shell + + ./run-qa-checks + +Install and run on docker +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Note**: This Docker image is for development purposes only. +For the official OpenWISP Docker images, see: `docker-openwisp +`_. + +Build from the Dockerfile: + +.. code-block:: shell + + docker-compose build + +Run the docker container: + +.. code-block:: shell + + docker-compose up diff --git a/docs/developer/management-commands.rst b/docs/developer/management-commands.rst new file mode 100644 index 00000000..5ea306a7 --- /dev/null +++ b/docs/developer/management-commands.rst @@ -0,0 +1,28 @@ +Management commands +------------------- + +``run_checks`` +~~~~~~~~~~~~~~ + +This command will execute all the `available checks <#available-checks>`_ for all the devices. +By default checks are run periodically by *celery beat*. You can learn more +about this in `Setup <#setup-integrate-in-an-existing-django-project>`_. + +Example usage: + +.. code-block:: shell + + cd tests/ + ./manage.py run_checks + +``migrate_timeseries`` +~~~~~~~~~~~~~~~~~~~~~~ + +This command triggers asynchronous migration of the time-series database. + +Example usage: + +.. code-block:: shell + + cd tests/ + ./manage.py migrate_timeseries diff --git a/docs/developer/monitoring-scripts.rst b/docs/developer/monitoring-scripts.rst new file mode 100644 index 00000000..e40c708d --- /dev/null +++ b/docs/developer/monitoring-scripts.rst @@ -0,0 +1,40 @@ +Monitoring scripts +------------------ + +Monitoring scripts are now deprecated in favour of `monitoring packages `_. +Follow the migration guide in `Migrating from monitoring scripts to monitoring packages <#migrating-from-monitoring-scripts-to-monitoring-packages>`_ +section of this documentation. + +Migrating from monitoring scripts to monitoring packages +-------------------------------------------------------- + +This section is intended for existing users of *openwisp-monitoring*. +The older version of *openwisp-monitoring* used *monitoring scripts* that +are now deprecated in favour of `monitoring packages `_. + +If you already had a *monitoring template* created on your installation, +then the migrations of *openwisp-monitoring* will update that template +by making the following changes: + +- The file name of all scripts will be appended with ``legacy-`` keyword + in order to differentiate them from the scripts bundled with the new packages. +- The ``/usr/sbin/legacy-openwisp-monitoring`` (previously ``/usr/sbin/openwisp-monitoring``) + script will be updated to exit if `openwisp-monitoring package `_ + is installed on the device. + +Install the `monitoring packages `_ +as mentioned in the `Install monitoring packages on device <#install-monitoring-packages-on-the-device>`_ +section of this documentation. + +After the proper configuration of the `openwisp-monitoring package `_ +on your device, you can remove the monitoring template from your devices. + +We suggest removing the monitoring template from the devices one at a time instead +of deleting the template. This ensures the correctness of +*openwisp monitoring package* configuration and you'll not miss out on +any monitoring data. + +**Note:** If you have made changes to the default monitoring template created +by *openwisp-monitoring* or you are using custom monitoring templates, then you should +remove such templates from the device before installing the +`monitoring packages `_. diff --git a/docs/developer/registering-new-notification-types.rst b/docs/developer/registering-new-notification-types.rst new file mode 100644 index 00000000..9bd57380 --- /dev/null +++ b/docs/developer/registering-new-notification-types.rst @@ -0,0 +1,10 @@ +Registering new notification types +---------------------------------- + +You can define your own notification types using ``register_notification_type`` function from OpenWISP +Notifications. For more information, see the relevant `openwisp-notifications section about registering notification types +`_. + +Once a new notification type is registered, you have to use the `"notify" signal provided in +openwisp-notifications `_ +to send notifications for this type. diff --git a/docs/developer/registering-unregistering-chart-configuration.rst b/docs/developer/registering-unregistering-chart-configuration.rst new file mode 100644 index 00000000..53ec1423 --- /dev/null +++ b/docs/developer/registering-unregistering-chart-configuration.rst @@ -0,0 +1,73 @@ +Registering / Unregistering Chart Configuration +----------------------------------------------- + +**OpenWISP Monitoring** provides registering and unregistering chart configuration through utility functions +``openwisp_monitoring.monitoring.configuration.register_chart`` and ``openwisp_monitoring.monitoring.configuration.unregister_chart``. +Using these functions you can register or unregister chart configurations from anywhere in your code. + +``register_chart`` +~~~~~~~~~~~~~~~~~~ + +This function is used to register a new chart configuration from anywhere in your code. + ++--------------------------+-----------------------------------------------------+ +| **Parameter** | **Description** | ++--------------------------+-----------------------------------------------------+ +| **chart_name**: | A ``str`` defining name of the chart configuration. | ++--------------------------+-----------------------------------------------------+ +| **chart_configuration**: | A ``dict`` defining configuration of the chart. | ++--------------------------+-----------------------------------------------------+ + +An example usage has been shown below. + +.. code-block:: python + + from openwisp_monitoring.monitoring.configuration import register_chart + + # Define configuration of your chart + chart_config = { + 'type': 'histogram', + 'title': 'Histogram', + 'description': 'Histogram', + 'top_fields': 2, + 'order': 999, + 'query': { + 'influxdb': ( + "SELECT {fields|SUM|/ 1} FROM {key} " + "WHERE time >= '{time}' AND content_type = " + "'{content_type}' AND object_id = '{object_id}'" + ) + }, + } + + # Register your custom chart configuration + register_chart('chart_name', chart_config) + +**Note**: It will raise ``ImproperlyConfigured`` exception if a chart configuration +is already registered with same name (not to be confused with verbose_name). + +If you don't need to register a new chart but need to change a specific key of an +existing chart configuration, you can use `OPENWISP_MONITORING_CHARTS <#openwisp_monitoring_charts>`_. + +``unregister_chart`` +~~~~~~~~~~~~~~~~~~~~ + +This function is used to unregister a chart configuration from anywhere in your code. + ++------------------+-----------------------------------------------------+ +| **Parameter** | **Description** | ++------------------+-----------------------------------------------------+ +| **chart_name**: | A ``str`` defining name of the chart configuration. | ++------------------+-----------------------------------------------------+ + +An example usage is shown below. + +.. code-block:: python + + from openwisp_monitoring.monitoring.configuration import unregister_chart + + # Unregister previously registered chart configuration + unregister_chart('chart_name') + +**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned chart +configuration is not registered. diff --git a/docs/developer/registering-unregistering-metric-configuration.rst b/docs/developer/registering-unregistering-metric-configuration.rst new file mode 100644 index 00000000..4e21a01a --- /dev/null +++ b/docs/developer/registering-unregistering-metric-configuration.rst @@ -0,0 +1,169 @@ +Registering / Unregistering Metric Configuration +------------------------------------------------ + +**OpenWISP Monitoring** provides registering and unregistering metric configuration through utility functions +``openwisp_monitoring.monitoring.configuration.register_metric`` and ``openwisp_monitoring.monitoring.configuration.unregister_metric``. +Using these functions you can register or unregister metric configurations from anywhere in your code. + +``register_metric`` +~~~~~~~~~~~~~~~~~~~ + +This function is used to register a new metric configuration from anywhere in your code. + ++--------------------------+------------------------------------------------------+ +| **Parameter** | **Description** | ++--------------------------+------------------------------------------------------+ +| **metric_name**: | A ``str`` defining name of the metric configuration. | ++--------------------------+------------------------------------------------------+ +|**metric_configuration**: | A ``dict`` defining configuration of the metric. | ++--------------------------+------------------------------------------------------+ + +An example usage has been shown below. + +.. code-block:: python + + from django.utils.translation import gettext_lazy as _ + from openwisp_monitoring.monitoring.configuration import register_metric + + # Define configuration of your metric + metric_config = { + 'label': _('Ping'), + 'name': 'Ping', + 'key': 'ping', + 'field_name': 'reachable', + 'related_fields': ['loss', 'rtt_min', 'rtt_max', 'rtt_avg'], + 'charts': { + 'uptime': { + 'type': 'bar', + 'title': _('Uptime'), + 'description': _( + 'A value of 100% means reachable, 0% means unreachable, values in ' + 'between 0% and 100% indicate the average reachability in the ' + 'period observed. Obtained with the fping linux program.' + ), + 'summary_labels': [_('Average uptime')], + 'unit': '%', + 'order': 200, + 'colorscale': { + 'max': 100, + 'min': 0, + 'label': _('Reachable'), + 'scale': [ + [[0, '#c13000'], + [0.1,'cb7222'], + [0.5,'#deed0e'], + [0.9, '#7db201'], + [1, '#498b26']], + ], + 'map': [ + [100, '#498b26', _('Reachable')], + [90, '#7db201', _('Mostly Reachable')], + [50, '#deed0e', _('Partly Reachable')], + [10, '#cb7222', _('Mostly Unreachable')], + [None, '#c13000', _('Unreachable')], + ], + 'fixed_value': 100, + }, + 'query': chart_query['uptime'], + }, + 'packet_loss': { + 'type': 'bar', + 'title': _('Packet loss'), + 'description': _( + 'Indicates the percentage of lost packets observed in ICMP probes. ' + 'Obtained with the fping linux program.' + ), + 'summary_labels': [_('Average packet loss')], + 'unit': '%', + 'colors': '#d62728', + 'order': 210, + 'query': chart_query['packet_loss'], + }, + 'rtt': { + 'type': 'scatter', + 'title': _('Round Trip Time'), + 'description': _( + 'Round trip time observed in ICMP probes, measuered in milliseconds.' + ), + 'summary_labels': [ + _('Average RTT'), + _('Average Max RTT'), + _('Average Min RTT'), + ], + 'unit': _(' ms'), + 'order': 220, + 'query': chart_query['rtt'], + }, + }, + 'alert_settings': {'operator': '<', 'threshold': 1, 'tolerance': 0}, + 'notification': { + 'problem': { + 'verbose_name': 'Ping PROBLEM', + 'verb': 'cannot be reached anymore', + 'level': 'warning', + 'email_subject': _( + '[{site.name}] {notification.target} is not reachable' + ), + 'message': _( + 'The device [{notification.target}] {notification.verb} anymore by our ping ' + 'messages.' + ), + }, + 'recovery': { + 'verbose_name': 'Ping RECOVERY', + 'verb': 'has become reachable', + 'level': 'info', + 'email_subject': _( + '[{site.name}] {notification.target} is reachable again' + ), + 'message': _( + 'The device [{notification.target}] {notification.verb} again by our ping ' + 'messages.' + ), + }, + }, + } + + # Register your custom metric configuration + register_metric('ping', metric_config) + +The above example will register one metric configuration (named ``ping``), three chart +configurations (named ``rtt``, ``packet_loss``, ``uptime``) as defined in the **charts** key, +two notification types (named ``ping_recovery``, ``ping_problem``) as defined in **notification** key. + +The ``AlertSettings`` of ``ping`` metric will by default use ``threshold`` and ``tolerance`` +defined in the ``alert_settings`` key. +You can always override them and define your own custom values via the *admin*. + +You can also use the ``alert_field`` key in metric configuration +which allows ``AlertSettings`` to check the ``threshold`` on +``alert_field`` instead of the default ``field_name`` key. + +**Note**: It will raise ``ImproperlyConfigured`` exception if a metric configuration +is already registered with same name (not to be confused with verbose_name). + +If you don't need to register a new metric but need to change a specific key of an +existing metric configuration, you can use `OPENWISP_MONITORING_METRICS <#openwisp_monitoring_metrics>`_. + +``unregister_metric`` +~~~~~~~~~~~~~~~~~~~~~ + +This function is used to unregister a metric configuration from anywhere in your code. + ++------------------+------------------------------------------------------+ +| **Parameter** | **Description** | ++------------------+------------------------------------------------------+ +| **metric_name**: | A ``str`` defining name of the metric configuration. | ++------------------+------------------------------------------------------+ + +An example usage is shown below. + +.. code-block:: python + + from openwisp_monitoring.monitoring.configuration import unregister_metric + + # Unregister previously registered metric configuration + unregister_metric('metric_name') + +**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned metric +configuration is not registered. diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 00000000..3ac09acc --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,95 @@ +OpenWISP Monitoring +=================== + +OpenWISP Monitoring is a network monitoring system written in Python and Django, +designed to be **extensible**, **programmable**, **scalable** and easy to use by end users: +once the system is configured, monitoring checks, alerts and metric collection +happens automatically. + +See the `available features <#available-features>`_. + +`OpenWISP `_ is not only an application designed for end users, +but can also be used as a framework on which custom network automation solutions can be +built on top of its building blocks. + +Other popular building blocks that are part of the OpenWISP ecosystem are: + +- `openwisp-controller `_: + network and WiFi controller: provisioning, configuration management, + x509 PKI management and more; works on OpenWRT, but designed to work also on other systems. +- `openwisp-network-topology `_: + provides way to collect and visualize network topology data from + dynamic mesh routing daemons or other network software (eg: OpenVPN); + it can be used in conjunction with openwisp-monitoring to get a better idea + of the state of the network +- `openwisp-firmware-upgrader `_: + automated firmware upgrades (single device or mass network upgrades) +- `openwisp-radius `_: + based on FreeRADIUS, allows to implement network access authentication systems like + 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 (802.11u) +- `openwisp-ipam `_: + it allows to manage the IP address space of networks + +**For a more complete overview of the OpenWISP modules and architecture**, +see the +`OpenWISP Architecture Overview +`_. + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png + :align: center + +Available Features +------------------ + +* Collection of monitoring information in a timeseries database (currently only influxdb is supported) +* Allows to browse alerts easily from the user interface with one click +* Collects and displays `device status <#device-status>`_ information like + uptime, RAM status, CPU load averages, + Interface properties and addresses, WiFi interface status and associated clients, + Neighbors information, DHCP Leases, Disk/Flash status +* Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, + `round trip time (latency) <#ping>`_, + `associated wifi clients <#wifi-clients>`_, `interface traffic <#traffic>`_, + `RAM usage <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage <#disk-usage>`_, + mobile signal (LTE/UMTS/GSM `signal strength <#mobile-signal-strength>`_, + `signal quality <#mobile-signal-quality>`_, + `access technology in use <#mobile-access-technology-in-use>`_), `bandwidth <#iperf3>`_, + `transferred data <#iperf3>`_, `restransmits <#iperf3>`_, `jitter <#iperf3>`_, + `datagram <#iperf3>`_, `datagram loss <#iperf3>`_ +* 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 the last 1 day, 3 days, 7 days, 30 days, and 365 days +* Configurable alerts +* CSV Export of monitoring data +* An overview of the status of the network is shown in the admin dashboard, + a chart shows the percentages of devices which are online, offline or having issues; + there are also `two timeseries charts which show the total unique WiFI clients and + the traffic flowing to the network `_, + a geographic map is also available for those who use the geographic features of OpenWISP +* Possibility to configure additional `Metrics <#openwisp_monitoring_metrics>`_ and `Charts <#openwisp_monitoring_charts>`_ +* Extensible active check system: it's possible to write additional checks that + are run periodically using python classes +* Extensible metrics and charts: it's possible to define new metrics and new charts +* API to retrieve the chart metrics and status information of each device + based on `NetJSON DeviceMonitoring `_ +* `Iperf3 check <#iperf3-1>`_ that provides network performance measurements such as maximum + achievable bandwidth, jitter, datagram loss etc of the openwrt device using `iperf3 utility `_ + +.. toctree:: + :maxdepth: 1 + + ./user/quickstart.rst + ./user/passive-vs-active-metric-collection.rst + ./user/device-health-status.rst + ./user/default-metrics.rst + ./user/dashboard-monitoring-charts.rst + ./user/adaptive-size-charts.rst + ./user/wifi-sessions.rst + ./user/default-alerts-and-notifications.rst + ./user/available-checks.rst + ./user/iperf3-usage-instructions.rst + ./user/adding-checks-and-alertsettings.rst + ./user/settings.rst + ./user/rest-api.rst + ./developer/developer-docs.rst diff --git a/docs/user/adaptive-size-charts.rst b/docs/user/adaptive-size-charts.rst new file mode 100644 index 00000000..4c8a3c45 --- /dev/null +++ b/docs/user/adaptive-size-charts.rst @@ -0,0 +1,29 @@ +Adaptive size charts +-------------------- + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/adaptive-chart.png + :align: center + +When configuring charts, it is possible to flag their unit +as ``adaptive_prefix``, this allows to make the charts more readable because +the units are shown in either `K`, `M`, `G` and `T` depending on +the size of each point, the summary values and Y axis are also resized. + +Example taken from the default configuration of the traffic chart: + +.. code-block:: python + + 'traffic': { + # other configurations for this chart + + # traffic measured in 'B' (bytes) + # unit B, KB, MB, GB, TB + 'unit': 'adaptive_prefix+B', + }, + + 'bandwidth': { + # adaptive unit for bandwidth related charts + # bandwidth measured in 'bps'(bits/sec) + # unit bps, Kbps, Mbps, Gbps, Tbps + 'unit': 'adaptive_prefix+bps', + }, diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst new file mode 100644 index 00000000..ac7f5892 --- /dev/null +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -0,0 +1,70 @@ +Adding Checks and Alert settings from the device page +----------------------------------------------------- + +We can add checks and define alert settings directly from the **device page**. + +To add a check, you just need to select an available **check type** as shown below: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-check.png + :align: center + +The following example shows how to use the +`OPENWISP_MONITORING_METRICS setting <#openwisp_monitoring_metrics>`_ +to reconfigure the system for `iperf3 check <#iperf3-1>`_ to send an alert if +the measured **TCP bandwidth** has been less than **10 Mbit/s** for more than **2 days**. + +1. By default, `Iperf3 checks <#iperf3-1>`_ come with default alert settings, +but it is easy to customize alert settings through the device page as shown below: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-alertsettings.png + :align: center + +2. Now, add the following notification configuration to send an alert for **TCP bandwidth**: + +.. code-block:: python + + # Main project settings.py + from django.utils.translation import gettext_lazy as _ + + OPENWISP_MONITORING_METRICS = { + 'iperf3': { + 'notification': { + 'problem': { + 'verbose_name': 'Iperf3 PROBLEM', + 'verb': _('Iperf3 bandwidth is less than normal value'), + 'level': 'warning', + 'email_subject': _( + '[{site.name}] PROBLEM: {notification.target} {notification.verb}' + ), + 'message': _( + 'The device [{notification.target}]({notification.target_link}) ' + '{notification.verb}.' + ), + }, + 'recovery': { + 'verbose_name': 'Iperf3 RECOVERY', + 'verb': _('Iperf3 bandwidth now back to normal'), + 'level': 'info', + 'email_subject': _( + '[{site.name}] RECOVERY: {notification.target} {notification.verb}' + ), + 'message': _( + 'The device [{notification.target}]({notification.target_link}) ' + '{notification.verb}.' + ), + }, + }, + }, + } + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_warn.png + :align: center + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_info.png + :align: center + +**Note:** To access the features described above, the user must have permissions for ``Check`` and ``AlertSetting`` inlines, +these permissions are included by default in the "Administrator" and "Operator" groups and are shown in the screenshot below. + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/inline-permissions.png + :align: center diff --git a/docs/user/available-checks.rst b/docs/user/available-checks.rst new file mode 100644 index 00000000..9c875344 --- /dev/null +++ b/docs/user/available-checks.rst @@ -0,0 +1,49 @@ +Available Checks +---------------- + +Ping +~~~~ + +This check returns information on device ``uptime`` and ``RTT (Round trip time)``. +The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. The ``fping`` +command is used to collect these metrics. +You may choose to disable auto creation of this check by setting +`OPENWISP_MONITORING_AUTO_PING <#OPENWISP_MONITORING_AUTO_PING>`_ to ``False``. + +You can change the default values used for ping checks using +`OPENWISP_MONITORING_PING_CHECK_CONFIG <#OPENWISP_MONITORING_PING_CHECK_CONFIG>`_ setting. + +Configuration applied +~~~~~~~~~~~~~~~~~~~~~ + +This check ensures that the `openwisp-config agent `_ +is running and applying configuration changes in a timely manner. +You may choose to disable auto creation of this check by using the +setting `OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK <#OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK>`_. + +This check runs periodically, but it is also triggered whenever the +configuration status of a device changes, this ensures the check reacts +quickly to events happening in the network and informs the user promptly +if there's anything that is not working as intended. + +Iperf3 +~~~~~~ + +This check provides network performance measurements such as maximum achievable bandwidth, +jitter, datagram loss etc of the device using `iperf3 utility `_. + +This check is **disabled by default**. You can enable auto creation of this check by setting the +`OPENWISP_MONITORING_AUTO_IPERF3 <#OPENWISP_MONITORING_AUTO_IPERF3>`_ to ``True``. + +You can also `add the iperf3 check +<#add-checks-and-alert-settings-from-the-device-page>`_ directly from the device page. + +It also supports tuning of various parameters. + +You can also change the parameters used for iperf3 checks (e.g. timing, port, username, +password, rsa_publc_key etc) using the `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ setting. + +**Note:** When setting `OPENWISP_MONITORING_AUTO_IPERF3 <#OPENWISP_MONITORING_AUTO_IPERF3>`_ to ``True``, +you may need to update the `metric configuration <#add-checks-and-alert-settings-from-the-device-page>`_ +to enable alerts for the iperf3 check. diff --git a/docs/user/dashboard-monitoring-charts.rst b/docs/user/dashboard-monitoring-charts.rst new file mode 100644 index 00000000..ca0ea0d4 --- /dev/null +++ b/docs/user/dashboard-monitoring-charts.rst @@ -0,0 +1,15 @@ +Dashboard Monitoring Charts +--------------------------- + +.. figure:: https://github.com/openwisp/openwisp-monitoring/blob/docs/docs/1.1/dashboard-charts.png + :align: center + +OpenWISP Monitoring adds two timeseries charts to the admin dashboard: + +- **General WiFi clients Chart**: Shows the number of connected clients to the WiFi + interfaces of devices in the network. +- **General traffic Chart**: Shows the amount of traffic flowing in the network. + +You can configure the interfaces included in the **General traffic chart** using +the `"OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART" +<#openwisp_monitoring_dashboard_traffic_chart>`_ setting. diff --git a/docs/user/default-alerts-and-notifications.rst b/docs/user/default-alerts-and-notifications.rst new file mode 100644 index 00000000..c3117db7 --- /dev/null +++ b/docs/user/default-alerts-and-notifications.rst @@ -0,0 +1,17 @@ +Default Alerts / Notifications +------------------------------ + ++-------------------------------+------------------------------------------------------------------+ +| Notification Type | Use | ++-------------------------------+------------------------------------------------------------------+ +| ``threshold_crossed`` | Fires when a metric crosses the boundary defined in the | +| | threshold value of the alert settings. | ++-------------------------------+------------------------------------------------------------------+ +| ``threshold_recovery`` | Fires when a metric goes back within the expected range. | ++-------------------------------+------------------------------------------------------------------+ +| ``connection_is_working`` | Fires when the connection to a device is working. | ++-------------------------------+------------------------------------------------------------------+ +| ``connection_is_not_working`` | Fires when the connection (eg: SSH) to a device stops working | +| | (eg: credentials are outdated, management IP address is | +| | outdated, or device is not reachable). | ++-------------------------------+------------------------------------------------------------------+ diff --git a/docs/user/default-metrics.rst b/docs/user/default-metrics.rst new file mode 100644 index 00000000..8558b3c7 --- /dev/null +++ b/docs/user/default-metrics.rst @@ -0,0 +1,267 @@ +Default Metrics +--------------- + +Device Status +~~~~~~~~~~~~~ + +This metric stores the status of the device for viewing purposes. + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-1.png + :align: center + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-2.png + :align: center + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-3.png + :align: center + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-4.png + :align: center + +Ping +~~~~ + ++--------------------+----------------------------------------------------------------+ +| **measurement**: | ``ping`` | ++--------------------+----------------------------------------------------------------+ +| **types**: | ``int`` (reachable and loss), ``float`` (rtt) | ++--------------------+----------------------------------------------------------------+ +| **fields**: | ``reachable``, ``loss``, ``rtt_min``, ``rtt_max``, ``rtt_avg`` | ++--------------------+----------------------------------------------------------------+ +| **configuration**: | ``ping`` | ++--------------------+----------------------------------------------------------------+ +| **charts**: | ``uptime``, ``packet_loss``, ``rtt`` | ++--------------------+----------------------------------------------------------------+ + +**Uptime**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/uptime.png + :align: center + +**Packet loss**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/packet-loss.png + :align: center + +**Round Trip Time**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/rtt.png + :align: center + +Traffic +~~~~~~~ + ++--------------------+--------------------------------------------------------------------------+ +| **measurement**: | ``traffic`` | ++--------------------+--------------------------------------------------------------------------+ +| **type**: | ``int`` | ++--------------------+--------------------------------------------------------------------------+ +| **fields**: | ``rx_bytes``, ``tx_bytes`` | ++--------------------+--------------------------------------------------------------------------+ +| **tags**: | .. code-block:: python | +| | | +| | { | +| | 'organization_id': '', | +| | 'ifname': '', | +| | # optional | +| | 'location_id': '', | +| | 'floorplan_id': '', | +| | } | ++--------------------+--------------------------------------------------------------------------+ +| **configuration**: | ``traffic`` | ++--------------------+--------------------------------------------------------------------------+ +| **charts**: | ``traffic`` | ++--------------------+--------------------------------------------------------------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/traffic.png + :align: center + +WiFi Clients +~~~~~~~~~~~~ + ++--------------------+--------------------------------------------------------------------------+ +| **measurement**: | ``wifi_clients`` | ++--------------------+--------------------------------------------------------------------------+ +| **type**: | ``int`` | ++--------------------+--------------------------------------------------------------------------+ +| **fields**: | ``clients`` | ++--------------------+--------------------------------------------------------------------------+ +| **tags**: | .. code-block:: python | +| | | +| | { | +| | 'organization_id': '', | +| | 'ifname': '', | +| | # optional | +| | 'location_id': '', | +| | 'floorplan_id': '', | +| | } | ++--------------------+--------------------------------------------------------------------------+ +| **configuration**: | ``clients`` | ++--------------------+--------------------------------------------------------------------------+ +| **charts**: | ``wifi_clients`` | ++--------------------+--------------------------------------------------------------------------+ + + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-clients.png + :align: center + +Memory Usage +~~~~~~~~~~~~ + ++--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ +| **measurement**: | ```` | ++--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ +| **type**: | ``float`` | ++--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ +| **fields**: | ``percent_used``, ``free_memory``, ``total_memory``, ``buffered_memory``, ``shared_memory``, ``cached_memory``, ``available_memory`` | ++--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ +| **configuration**: | ``memory`` | ++--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ +| **charts**: | ``memory`` | ++--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/memory.png + :align: center + +CPU Load +~~~~~~~~ + ++--------------------+----------------------------------------------------+ +| **measurement**: | ``load`` | ++--------------------+----------------------------------------------------+ +| **type**: | ``float`` | ++--------------------+----------------------------------------------------+ +| **fields**: | ``cpu_usage``, ``load_1``, ``load_5``, ``load_15`` | ++--------------------+----------------------------------------------------+ +| **configuration**: | ``load`` | ++--------------------+----------------------------------------------------+ +| **charts**: | ``load`` | ++--------------------+----------------------------------------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/cpu-load.png + :align: center + +Disk Usage +~~~~~~~~~~ + ++--------------------+-------------------+ +| **measurement**: | ``disk`` | ++--------------------+-------------------+ +| **type**: | ``float`` | ++--------------------+-------------------+ +| **fields**: | ``used_disk`` | ++--------------------+-------------------+ +| **configuration**: | ``disk`` | ++--------------------+-------------------+ +| **charts**: | ``disk`` | ++--------------------+-------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/disk-usage.png + :align: center + +Mobile Signal Strength +~~~~~~~~~~~~~~~~~~~~~~ + ++--------------------+-----------------------------------------+ +| **measurement**: | ``signal_strength`` | ++--------------------+-----------------------------------------+ +| **type**: | ``float`` | ++--------------------+-----------------------------------------+ +| **fields**: | ``signal_strength``, ``signal_power`` | ++--------------------+-----------------------------------------+ +| **configuration**: | ``signal_strength`` | ++--------------------+-----------------------------------------+ +| **charts**: | ``signal_strength`` | ++--------------------+-----------------------------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-strength.png + :align: center + +Mobile Signal Quality +~~~~~~~~~~~~~~~~~~~~~~ + ++--------------------+-----------------------------------------+ +| **measurement**: | ``signal_quality`` | ++--------------------+-----------------------------------------+ +| **type**: | ``float`` | ++--------------------+-----------------------------------------+ +| **fields**: | ``signal_quality``, ``signal_quality`` | ++--------------------+-----------------------------------------+ +| **configuration**: | ``signal_quality`` | ++--------------------+-----------------------------------------+ +| **charts**: | ``signal_quality`` | ++--------------------+-----------------------------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-quality.png + :align: center + +Mobile Access Technology in use +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------------+-------------------+ +| **measurement**: | ``access_tech`` | ++--------------------+-------------------+ +| **type**: | ``int`` | ++--------------------+-------------------+ +| **fields**: | ``access_tech`` | ++--------------------+-------------------+ +| **configuration**: | ``access_tech`` | ++--------------------+-------------------+ +| **charts**: | ``access_tech`` | ++--------------------+-------------------+ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/access-technology.png + :align: center + +Iperf3 +~~~~~~ + ++--------------------+---------------------------------------------------------------------------------------------------------------------------+ +| **measurement**: | ``iperf3`` | ++--------------------+---------------------------------------------------------------------------------------------------------------------------+ +| **types**: | | ``int`` (iperf3_result, sent_bytes_tcp, received_bytes_tcp, retransmits, sent_bytes_udp, total_packets, lost_packets), | +| | | ``float`` (sent_bps_tcp, received_bps_tcp, sent_bps_udp, jitter, lost_percent) | ++--------------------+---------------------------------------------------------------------------------------------------------------------------+ +| **fields**: | | ``iperf3_result``, ``sent_bps_tcp``, ``received_bps_tcp``, ``sent_bytes_tcp``, ``received_bytes_tcp``, ``retransmits``, | +| | | ``sent_bps_udp``, ``sent_bytes_udp``, ``jitter``, ``total_packets``, ``lost_packets``, ``lost_percent`` | ++--------------------+---------------------------------------------------------------------------------------------------------------------------+ +| **configuration**: | ``iperf3`` | ++--------------------+---------------------------------------------------------------------------------------------------------------------------+ +| **charts**: | ``bandwidth``, ``transfer``, ``retransmits``, ``jitter``, ``datagram``, ``datagram_loss`` | ++--------------------+---------------------------------------------------------------------------------------------------------------------------+ + +**Bandwidth**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/bandwidth.png + :align: center + +**Transferred Data**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/transferred-data.png + :align: center + +**Retransmits**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/retransmits.png + :align: center + +**Jitter**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/jitter.png + :align: center + +**Datagram**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram.png + :align: center + +**Datagram loss**: + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram-loss.png + :align: center + +For more info on how to configure and use Iperf3, please refer to +`iperf3 check usage instructions <#iperf3-check-usage-instructions>`_. + +**Note:** Iperf3 charts uses ``connect_points=True`` in +`default chart configuration <#openwisp_monitoring_charts>`_ that joins it's individual chart data points. diff --git a/docs/user/device-health-status.rst b/docs/user/device-health-status.rst new file mode 100644 index 00000000..edab4d8f --- /dev/null +++ b/docs/user/device-health-status.rst @@ -0,0 +1,35 @@ +Device Health Status +-------------------- + +The possible values for the health status field (``DeviceMonitoring.status``) +are explained below. + +``UNKNOWN`` +~~~~~~~~~~~ + +Whenever a new device is created it will have ``UNKNOWN`` as it's default Heath Status. + +It implies that the system doesn't know whether the device is reachable yet. + +``OK`` +~~~~~~ + +Everything is working normally. + +``PROBLEM`` +~~~~~~~~~~~ + +One of the metrics has a value which is not in the expected range +(the threshold value set in the alert settings has been crossed). + +Example: CPU usage should be less than 90% but current value is at 95%. + +``CRITICAL`` +~~~~~~~~~~~~ + +One of the metrics defined in ``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` +has a value which is not in the expected range +(the threshold value set in the alert settings has been crossed). + +Example: ping is by default a critical metric which is expected to be always 1 +(reachable). diff --git a/docs/user/iperf3-usage-instructions.rst b/docs/user/iperf3-usage-instructions.rst new file mode 100644 index 00000000..71757fd4 --- /dev/null +++ b/docs/user/iperf3-usage-instructions.rst @@ -0,0 +1,284 @@ +Iperf3 Check Usage Instructions +------------------------------- + +1. Make sure iperf3 is installed on the device +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Register your device to OpenWISP and make sure the `iperf3 openwrt package +`_ is installed on the device, +eg: + +.. code-block:: shell + + opkg install iperf3 # if using without authentication + opkg install iperf3-ssl # if using with authentication (read below for more info) + +2. Ensure SSH access from OpenWISP is enabled on your devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Follow the steps in `"How to configure push updates" section of the +OpenWISP documentation +`_ +to allow SSH access to you device from OpenWISP. + +**Note:** Make sure device connection is enabled +& working with right update strategy i.e. ``OpenWRT SSH``. + +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png + :alt: Enable ssh access from openwisp to device + :align: center + +3. Set up and configure Iperf3 server settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After having deployed your Iperf3 servers, you need to +configure the iperf3 settings on the django side of OpenWISP, +see the `test project settings for reference +`_. + +The host can be specified by hostname, IPv4 literal, or IPv6 literal. +Example: + +.. code-block:: python + + OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { + # 'org_pk' : {'host' : [], 'client_options' : {}} + 'a9734710-db30-46b0-a2fc-01f01046fe4f': { + # Some public iperf3 servers + # https://iperf.fr/iperf-servers.php#public-servers + 'host': ['iperf3.openwisp.io', '2001:db8::1', '192.168.5.2'], + 'client_options': { + 'port': 5209, + 'udp': {'bitrate': '30M'}, + 'tcp': {'bitrate': '0'}, + }, + }, + # another org + 'b9734710-db30-46b0-a2fc-01f01046fe4f': { + # available iperf3 servers + 'host': ['iperf3.openwisp2.io', '192.168.5.3'], + 'client_options': { + 'port': 5207, + 'udp': {'bitrate': '50M'}, + 'tcp': {'bitrate': '20M'}, + }, + }, + } + +**Note:** If an organization has more than one iperf3 server configured, then it enables +the iperf3 checks to run concurrently on different devices. If all of the available servers +are busy, then it will add the check back in the queue. + +The celery-beat configuration for the iperf3 check needs to be added too: + +.. code-block:: python + + from celery.schedules import crontab + + # Celery TIME_ZONE should be equal to django TIME_ZONE + # In order to schedule run_iperf3_checks on the correct time intervals + CELERY_TIMEZONE = TIME_ZONE + CELERY_BEAT_SCHEDULE = { + # Other celery beat configurations + # Celery beat configuration for iperf3 check + 'run_iperf3_checks': { + 'task': 'openwisp_monitoring.check.tasks.run_checks', + # https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html#crontab-schedules + # Executes check every 5 mins from 00:00 AM to 6:00 AM (night) + 'schedule': crontab(minute='*/5', hour='0-6'), + # Iperf3 check path + 'args': (['openwisp_monitoring.check.classes.Iperf3'],), + 'relative': True, + } + } + +Once the changes are saved, you will need to restart all the processes. + +**Note:** We recommended to configure this check to run in non peak +traffic times to not interfere with standard traffic. + +4. Run the check +~~~~~~~~~~~~~~~~ + +This should happen automatically if you have celery-beat correctly +configured and running in the background. +For testing purposes, you can run this check manually using the +`run_checks <#run_checks>`_ command. + +After that, you should see the iperf3 network measurements charts. + +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png + :alt: Iperf3 network measurement charts + +Iperf3 check parameters +~~~~~~~~~~~~~~~~~~~~~~~ + +Currently, iperf3 check supports the following parameters: + ++-----------------------+----------+--------------------------------------------------------------------+ +| **Parameter** | **Type** | **Default Value** | ++-----------------------+----------+--------------------------------------------------------------------+ +|``host`` | ``list`` | ``[]`` | ++-----------------------+----------+--------------------------------------------------------------------+ +|``username`` | ``str`` | ``''`` | ++-----------------------+----------+--------------------------------------------------------------------+ +|``password`` | ``str`` | ``''`` | ++-----------------------+----------+--------------------------------------------------------------------+ +|``rsa_public_key`` | ``str`` | ``''`` | ++-----------------------+----------+--------------------------------------------------------------------+ +|``client_options`` | +---------------------+----------+------------------------------------------+ | +| | | **Parameters** | **Type** | **Default Value** | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``port`` | ``int`` | ``5201`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``time`` | ``int`` | ``10`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``bytes`` | ``str`` | ``''`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``blockcount`` | ``str`` | ``''`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``window`` | ``str`` | ``0`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``parallel`` | ``int`` | ``1`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``reverse`` | ``bool`` | ``False`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``bidirectional`` | ``bool`` | ``False`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``connect_timeout`` | ``int`` | ``1000`` | | +| | +---------------------+----------+------------------------------------------+ | +| | | ``tcp`` | +----------------+----------+---------------------+ | | +| | | | | **Parameters** | **Type** | **Default Value** | | | +| | | | +----------------+----------+---------------------+ | | +| | | | |``bitrate`` | ``str`` | ``0`` | | | +| | | | +----------------+----------+---------------------+ | | +| | | | |``length`` | ``str`` | ``128K`` | | | +| | | | +----------------+----------+---------------------+ | | +| | +---------------------+-----------------------------------------------------+ | +| | | ``udp`` | +----------------+----------+---------------------+ | | +| | | | | **Parameters** | **Type** | **Default Value** | | | +| | | | +----------------+----------+---------------------+ | | +| | | | |``bitrate`` | ``str`` | ``30M`` | | | +| | | | +----------------+----------+---------------------+ | | +| | | | |``length`` | ``str`` | ``0`` | | | +| | | | +----------------+----------+---------------------+ | | +| | +---------------------+-----------------------------------------------------+ | ++-----------------------+-------------------------------------------------------------------------------+ + +To learn how to use these parameters, please see the +`iperf3 check configuration example <#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_. + +Visit the `official documentation `_ +to learn more about the iperf3 parameters. + +Iperf3 authentication +~~~~~~~~~~~~~~~~~~~~~ + +By default iperf3 check runs without any kind of **authentication**, +in this section we will explain how to configure **RSA authentication** +between the **client** and the **server** to restrict connections +to authenticated clients. + +Server side +########### + +1. Generate RSA keypair +^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: shell + + openssl genrsa -des3 -out private.pem 2048 + openssl rsa -in private.pem -outform PEM -pubout -out public_key.pem + openssl rsa -in private.pem -out private_key.pem -outform PEM + +After running the commands mentioned above, the public key will be stored in +``public_key.pem`` which will be used in **rsa_public_key** parameter +in `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ +and the private key will be contained in the file ``private_key.pem`` +which will be used with **--rsa-private-key-path** command option when +starting the iperf3 server. + +2. Create user credentials +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: shell + + USER=iperfuser PASSWD=iperfpass + echo -n "{$USER}$PASSWD" | sha256sum | awk '{ print $1 }' + ---- + ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 #This is the hash of "iperfuser" + +Add the above hash with username in ``credentials.csv`` + +.. code-block:: shell + + # file format: username,sha256 + iperfuser,ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 + +3. Now start the iperf3 server with auth options +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: shell + + iperf3 -s --rsa-private-key-path ./private_key.pem --authorized-users-path ./credentials.csv + +Client side (OpenWrt device) +############################ + +1. Install iperf3-ssl +^^^^^^^^^^^^^^^^^^^^^ + +Install the `iperf3-ssl openwrt package +`_ +instead of the normal +`iperf3 openwrt package `_ +because the latter comes without support for authentication. + +You may also check your installed **iperf3 openwrt package** features: + +.. code-block:: shell + + root@vm-openwrt:~ iperf3 -v + iperf 3.7 (cJSON 1.5.2) + Linux vm-openwrt 4.14.171 #0 SMP Thu Feb 27 21:05:12 2020 x86_64 + Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, + sendfile / zerocopy, socket pacing, authentication # contains 'authentication' + +2. Configure iperf3 check auth parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now, add the following iperf3 authentication parameters +to `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ +in the settings: + +.. code-block:: python + + OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { + 'a9734710-db30-46b0-a2fc-01f01046fe4f': { + 'host': ['iperf1.openwisp.io', 'iperf2.openwisp.io', '192.168.5.2'], + # All three parameters (username, password, rsa_publc_key) + # are required for iperf3 authentication + 'username': 'iperfuser', + 'password': 'iperfpass', + # Add RSA public key without any headers + # ie. -----BEGIN PUBLIC KEY-----, -----BEGIN END KEY----- + 'rsa_public_key': ( + """ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwuEm+iYrfSWJOupy6X3N + dxZvUCxvmoL3uoGAs0O0Y32unUQrwcTIxudy38JSuCccD+k2Rf8S4WuZSiTxaoea + 6Du99YQGVZeY67uJ21SWFqWU+w6ONUj3TrNNWoICN7BXGLE2BbSBz9YaXefE3aqw + GhEjQz364Itwm425vHn2MntSp0weWb4hUCjQUyyooRXPrFUGBOuY+VvAvMyAG4Uk + msapnWnBSxXt7Tbb++A5XbOMdM2mwNYDEtkD5ksC/x3EVBrI9FvENsH9+u/8J9Mf + 2oPl4MnlCMY86MQypkeUn7eVWfDnseNky7TyC0/IgCXve/iaydCCFdkjyo1MTAA4 + BQIDAQAB + """ + ), + 'client_options': { + 'port': 5209, + 'udp': {'bitrate': '20M'}, + 'tcp': {'bitrate': '0'}, + }, + } + } diff --git a/docs/user/passive-vs-active-metric-collection.rst b/docs/user/passive-vs-active-metric-collection.rst new file mode 100644 index 00000000..64fd999d --- /dev/null +++ b/docs/user/passive-vs-active-metric-collection.rst @@ -0,0 +1,19 @@ +Passive vs Active Metric Collection +----------------------------------- + +The `the different device metric +`_ +collected by OpenWISP Monitoring can be divided in two categories: + +1. **metrics collected actively by OpenWISP**: + these metrics are collected by the celery workers running on the + OpenWISP server, which continuously sends network requests to the + devices and store the results; +2. **metrics collected passively by OpenWISP**: + these metrics are sent by the + `openwrt-openwisp-monitoring agent <#install-monitoring-packages-on-the-device>`_ + installed on the network devices and are collected by OpenWISP via + its REST API. + +The `"Available Checks" <#available-checks>`_ section of this document +lists the currently implemented **active checks**. diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst new file mode 100644 index 00000000..bbc453fb --- /dev/null +++ b/docs/user/quickstart.rst @@ -0,0 +1,136 @@ +Quickstart Guide +---------------- + +Install OpenWISP Monitoring +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install *OpenWISP Monitoring* using one of the methods mentioned in the +`"Installation instructions" <#installation-instructions>`_. + +Install openwisp-config on the device +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`Install the openwisp-config agent for OpenWrt +`_ +on your device. + +Install monitoring packages on the device +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`Install the openwrt-openwisp-monitoring packages +`_ +on your device. + +These packages collect and send the +monitoring data from the device to OpenWISP Monitoring and +are required to collect `metrics <#openwisp_monitoring_metrics>`_ +like interface traffic, WiFi clients, CPU load, memory usage, etc. + +**Note**: if you are an existing user of *openwisp-monitoring* and are using +the legacy *monitoring template* for collecting metrics, we highly recommend +`Migrating from monitoring scripts to monitoring packages +<#migrating-from-monitoring-scripts-to-monitoring-packages>`_. + +Make sure OpenWISP can reach your devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to perform `active checks <#available-checks>`_ and other actions like +`triggering the push of configuration changes +`_, +`executing shell commands +`_ or +`performing firmware upgrades +`_, +**the OpenWISP server needs to be able to reach the network devices**. + +There are mainly two deployment scenarios for OpenWISP: + +1. the OpenWISP server is deployed on the public internet and the devices are + geographically distributed across different locations: + **in this case a management tunnel is needed** +2. the OpenWISP server is deployed on a computer/server which is located in + the same Layer 2 network (that is, in the same LAN) where the devices + are located. + **in this case a management tunnel is NOT needed** + +1. Public internet deployment +############################# + +This is the most common scenario: + +- the OpenWISP server is deployed to the public internet, hence the + server has a public IPv4 (and IPv6) address and usually a valid + SSL certificate provided by Mozilla Letsencrypt or another SSL provider +- the network devices are geographically distributed across different + locations (different cities, different regions, different countries) + +In this scenario, the OpenWISP application will not be able to reach the +devices **unless a management tunnel** is used, for that reason having +a management VPN like OpenVPN, Wireguard or any other tunneling solution +is paramount, not only to allow OpenWISP to work properly, but also to +be able to perform debugging and troubleshooting when needed. + +In this scenario, the following requirements are needed: + +- a VPN server must be installed in a way that the OpenWISP + server can reach the VPN peers, for more information on how to do this + via OpenWISP please refer to the following sections: + + - `OpenVPN tunnel automation + `_ + - `Wireguard tunnel automation + `_ + + If you prefer to use other tunneling solutions (L2TP, Softether, etc.) + and know how to configure those solutions on your own, + that's totally fine as well. + + If the OpenWISP server is connected to a network infrastructure + which allows it to reach the devices via pre-existing tunneling or + Intranet solutions (eg: MPLS, SD-WAN), then setting up a VPN server + is not needed, as long as there's a dedicated interface on OpenWrt + which gets an IP address assigned to it and which is reachable from + the OpenWISP server. + +- The devices must be configured to join the management tunnel automatically, + either via a pre-existing configuration in the firmware or via an + `OpenWISP Template `_. + +- The `openwisp-config `_ + agent on the devices must be configured to specify + the ``management_interface`` option, the agent will communicate the + IP of the management interface to the OpenWISP Server and OpenWISP will + use the management IP for reaching the device. + + For example, if the *management interface* is named ``tun0``, + the openwisp-config configuration should look like the following example: + +.. code-block:: text + + # In /etc/config/openwisp on the device + + config controller 'http' + # ... other configuration directives ... + option management_interface 'tun0' + +2. LAN deployment +################# + +When the OpenWISP server and the network devices are deployed in the same +L2 network (eg: an office LAN) and the OpenWISP server is reachable +on the LAN address, OpenWISP can then use the **Last IP** field of the +devices to reach them. + +In this scenario it's necessary to set the +`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" <#openwisp-monitoring-management-ip-only>`_ +setting to ``False``. + +Creating checks for a device +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the `active checks <#available-checks>`_ are created +automatically for all devices, unless the automatic creation of some +specific checks has been disabled, for more information on how to do this, +refer to the `active checks <#available-checks>`_ section. + +These checks are created and executed in the background by celery workers. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst new file mode 100644 index 00000000..d99b9894 --- /dev/null +++ b/docs/user/rest-api.rst @@ -0,0 +1,262 @@ +Rest API +-------- + +Live documentation +~~~~~~~~~~~~~~~~~~ + +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png + +A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. + +Browsable web interface +~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png + +Additionally, opening any of the endpoints `listed below <#list-of-endpoints>`_ +directly in the browser will show the `browsable API interface of Django-REST-Framework +`_, +which makes it even easier to find out the details of each endpoint. + +List of endpoints +~~~~~~~~~~~~~~~~~ + +Since the detailed explanation is contained in the `Live documentation <#live-documentation>`_ +and in the `Browsable web page <#browsable-web-interface>`_ of each point, +here we'll provide just a list of the available endpoints, +for further information please open the URL of the endpoint in your browser. + +Retrieve general monitoring charts +################################## + +.. code-block:: text + + GET /api/v1/monitoring/dashboard/ + +This API endpoint is used to show dashboard monitoring charts. It supports +multi-tenancy and allows filtering monitoring data by ``organization_slug``, +``location_id`` and ``floorplan_id`` e.g.: + +.. code-block:: text + + GET /api/v1/monitoring/dashboard/?organization_slug=,&location_id=,&floorplan_id=, + +- When retrieving chart data, the ``time`` parameter allows to specify + the time frame, eg: + + - ``1d``: returns data of the last day + - ``3d``: returns data of the last 3 days + - ``7d``: returns data of the last 7 days + - ``30d``: returns data of the last 30 days + - ``365d``: returns data of the last 365 days + +- In alternative to ``time`` it is possible to request chart data for a custom + date range by using the ``start`` and ``end`` parameters, eg: + +.. code-block:: text + + GET /api/v1/monitoring/dashboard/?start={start_datetime}&end={end_datetime} + +**Note**: ``start`` and ``end`` parameters should be in the format +``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. + +Retrieve device charts and device status data +############################################# + +.. code-block:: text + + GET /api/v1/monitoring/device/{pk}/?key={key}&status=true&time={timeframe} + +The format used for Device Status is inspired by +`NetJSON DeviceMonitoring `_. + +**Notes**: + +- If the request is made without ``?status=true`` the response will + contain only charts data and will not include any device status information + (current load average, ARP table, DCHP leases, etc.). + +- When retrieving chart data, the ``time`` parameter allows to specify + the time frame, eg: + + - ``1d``: returns data of the last day + - ``3d``: returns data of the last 3 days + - ``7d``: returns data of the last 7 days + - ``30d``: returns data of the last 30 days + - ``365d``: returns data of the last 365 days + +- In alternative to ``time`` it is possible to request chart data for a custom + date range by using the ``start`` and ``end`` parameters, eg: + +- The response contains device information, monitoring status (health status), + a list of metrics with their respective statuses, chart data and + device status information (only if ``?status=true``). + +- This endpoint can be accessed with session authentication, token authentication, + or alternatively with the device key passed as query string parameter + as shown below (`?key={key}`); + note: this method is meant to be used by the devices. + +.. code-block:: text + + GET /api/v1/monitoring/device/{pk}/?key={key}&status=true&start={start_datetime}&end={end_datetime} + +**Note**: ``start`` and ``end`` parameters must be in the format +``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. + +List device monitoring information +################################## + +.. code-block:: text + + GET /api/v1/monitoring/device/ + +**Notes**: + +- The response contains device information and monitoring status (health status), + but it does not include the information and + health status of the specific metrics, this information + can be retrieved in the detail endpoint of each device. + +- This endpoint can be accessed with session authentication and token authentication. + +**Available filters** + +Data can be filtered by health status (e.g. critical, ok, problem, and unknown) +to obtain the list of devices in the corresponding status, for example, +to retrieve the list of devices which are in critical conditions +(eg: unreachable), the following will work: + +.. code-block:: text + + GET /api/v1/monitoring/device/?monitoring__status=critical + +To filter a list of device monitoring data based +on their organization, you can use the ``organization_id``. + +.. code-block:: text + + GET /api/v1/monitoring/device/?organization={organization_id} + +To filter a list of device monitoring data based +on their organization slug, you can use the ``organization_slug``. + +.. code-block:: text + + GET /api/v1/monitoring/device/?organization_slug={organization_slug} + +Collect device metrics and status +################################# + +.. code-block:: text + + POST /api/v1/monitoring/device/{pk}/?key={key}&time={datetime} + +If data is latest then an additional parameter current can also be passed. For e.g.: + +.. code-block:: text + + POST /api/v1/monitoring/device/{pk}/?key={key}&time={datetime}¤t=true + +The format used for Device Status is inspired by +`NetJSON DeviceMonitoring `_. + +**Note**: the device data will be saved in the timeseries database using +the date time specified ``time``, this should be in the format +``%d-%m-%Y_%H:%M:%S.%f``, otherwise 400 Bad Response will be returned. + +If the request is made without passing the ``time`` argument, +the server local time will be used. + +The ``time`` parameter was added to support `resilient collection +and sending of data by the OpenWISP Monitoring Agent +`_, +this feature allows sending data collected while the device is offline. + +List nearby devices +################### + +.. code-block:: text + + GET /api/v1/monitoring/device/{pk}/nearby-devices/ + +Returns list of nearby devices along with respective distance (in metres) and +monitoring status. + +**Available filters** + +The list of nearby devices provides the following filters: + +- ``organization`` (Organization ID of the device) +- ``organization__slug`` (Organization slug of the device) +- ``monitoring__status`` (Monitoring status (``unknown``, ``ok``, ``problem``, or ``critical``)) +- ``model`` (Pipe `|` separated list of device models) +- ``distance__lte`` (Distance in metres) + +Here's a few examples: + +.. code-block:: text + + GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization={organization_id} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization__slug={organization_slug} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?monitoring__status={monitoring_status} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance} + +List wifi session +################# + +.. code-block:: text + + GET /api/v1/monitoring/wifi-session/ + +**Available filters** + +The list of wifi session provides the following filters: + +- ``device__organization`` (Organization ID of the device) +- ``device`` (Device ID) +- ``device__group`` (Device group ID) +- ``start_time`` (Start time of the wifi session) +- ``stop_time`` (Stop time of the wifi session) + +Here's a few examples: + +.. code-block:: text + + GET /api/v1/monitoring/wifi-session/?device__organization={organization_id} + GET /api/v1/monitoring/wifi-session/?device={device_id} + GET /api/v1/monitoring/wifi-session/?device__group={group_id} + GET /api/v1/monitoring/wifi-session/?start_time={stop_time} + GET /api/v1/monitoring/wifi-session/?stop_time={stop_time} + +**Note:** Both `start_time` and `stop_time` support +greater than or equal to, as well as less than or equal to, filter lookups. + +For example: + +.. code-block:: text + + GET /api/v1/monitoring/wifi-session/?start_time__gt={start_time} + GET /api/v1/monitoring/wifi-session/?start_time__gte={start_time} + GET /api/v1/monitoring/wifi-session/?stop_time__lt={stop_time} + GET /api/v1/monitoring/wifi-session/?stop_time__lte={stop_time} + +Get wifi session +################ + +.. code-block:: text + + GET /api/v1/monitoring/wifi-session/{id}/ + +Pagination +########## + +Wifi session endpoint support the ``page_size`` parameter +that allows paginating the results in conjunction with the page parameter. + +.. code-block:: text + + GET /api/v1/monitoring/wifi-session/?page_size=10 + GET /api/v1/monitoring/wifi-session/?page_size=10&page=1 diff --git a/docs/user/settings.rst b/docs/user/settings.rst new file mode 100644 index 00000000..c5ce197e --- /dev/null +++ b/docs/user/settings.rst @@ -0,0 +1,749 @@ +Settings +-------- + +``TIMESERIES_DATABASE`` +~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``str`` | ++--------------+-----------+ +| **default**: | see below | ++--------------+-----------+ + +.. code-block:: python + + TIMESERIES_DATABASE = { + 'BACKEND': 'openwisp_monitoring.db.backends.influxdb', + 'USER': 'openwisp', + 'PASSWORD': 'openwisp', + 'NAME': 'openwisp2', + 'HOST': 'localhost', + 'PORT': '8086', + 'OPTIONS': { + 'udp_writes': False, + 'udp_port': 8089, + } + } + +The following table describes all keys available in ``TIMESERIES_DATABASE`` +setting: + ++---------------+--------------------------------------------------------------------------------------+ +| **Key** | ``Description`` | ++---------------+--------------------------------------------------------------------------------------+ +| ``BACKEND`` | The timeseries database backend to use. You can select one of the backends | +| | located in ``openwisp_monitoring.db.backends`` | ++---------------+--------------------------------------------------------------------------------------+ +| ``USER`` | User for logging into the timeseries database | ++---------------+--------------------------------------------------------------------------------------+ +| ``PASSWORD`` | Password of the timeseries database user | ++---------------+--------------------------------------------------------------------------------------+ +| ``NAME`` | Name of the timeseries database | ++---------------+--------------------------------------------------------------------------------------+ +| ``HOST`` | IP address/hostname of machine where the timeseries database is running | ++---------------+--------------------------------------------------------------------------------------+ +| ``PORT`` | Port for connecting to the timeseries database | ++---------------+--------------------------------------------------------------------------------------+ +| ``OPTIONS`` | These settings depends on the timeseries backend: | +| | | +| | +-----------------+----------------------------------------------------------------+ | +| | | ``udp_writes`` | Whether to use UDP for writing data to the timeseries database | | +| | +-----------------+----------------------------------------------------------------+ | +| | | ``udp_port`` | Timeseries database port for writing data using UDP | | +| | +-----------------+----------------------------------------------------------------+ | ++---------------+--------------------------------------------------------------------------------------+ + +**Note:** UDP packets can have a maximum size of 64KB. When using UDP for writing timeseries +data, if the size of the data exceeds 64KB, TCP mode will be used instead. + +**Note:** If you want to use the ``openwisp_monitoring.db.backends.influxdb`` backend +with UDP writes enabled, then you need to enable two different ports for UDP +(each for different retention policy) in your InfluxDB configuration. The UDP configuration +section of your InfluxDB should look similar to the following: + +.. code-block:: text + + # For writing data with the "default" retention policy + [[udp]] + enabled = true + bind-address = "127.0.0.1:8089" + database = "openwisp2" + + # For writing data with the "short" retention policy + [[udp]] + enabled = true + bind-address = "127.0.0.1:8090" + database = "openwisp2" + retention-policy = 'short' + +If you are using `ansible-openwisp2 `_ +for deploying OpenWISP, you can set the ``influxdb_udp_mode`` ansible variable to ``true`` +in your playbook, this will make the ansible role automatically configure the InfluxDB UDP listeners. +You can refer to the `ansible-ow-influxdb's `_ +(a dependency of ansible-openwisp2) documentation to learn more. + +``OPENWISP_MONITORING_DEFAULT_RETENTION_POLICY`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------------+ +| **type**: | ``str`` | ++--------------+--------------------------+ +| **default**: | ``26280h0m0s`` (3 years) | ++--------------+--------------------------+ + +The default retention policy that applies to the timeseries data. + +``OPENWISP_MONITORING_SHORT_RETENTION_POLICY`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``str`` | ++--------------+-------------+ +| **default**: | ``24h0m0s`` | ++--------------+-------------+ + +The default retention policy used to store raw device data. + +This data is only used to assess the recent status of devices, keeping +it for a long time would not add much benefit and would cost a lot more +in terms of disk space. + +``OPENWISP_MONITORING_AUTO_PING`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Whether ping checks are created automatically for devices. + +``OPENWISP_MONITORING_PING_CHECK_CONFIG`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``dict`` | ++--------------+-------------+ +| **default**: | ``{}`` | ++--------------+-------------+ + +This setting allows to override the default ping check configuration defined in +``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``. + +For example, if you want to change only the **timeout** of +``ping`` you can use: + +.. code-block:: python + + OPENWISP_MONITORING_PING_CHECK_CONFIG = { + 'timeout': { + 'default': 1000, + }, + } + +If you are overriding the default value for any parameter +beyond the maximum or minimum value defined in +``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``, +you will also need to override the ``maximum`` or ``minimum`` fields +as following: + +.. code-block:: python + + OPENWISP_MONITORING_PING_CHECK_CONFIG = { + 'timeout': { + 'default': 2000, + 'minimum': 1500, + 'maximum': 2500, + }, + } + +**Note:** Above ``maximum`` and ``minimum`` values are only used for +validating custom parameters of a ``Check`` object. + +``OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +This setting allows you to choose whether `config_applied <#configuration-applied>`_ checks should be +created automatically for newly registered devices. It's enabled by default. + +``OPENWISP_MONITORING_CONFIG_CHECK_INTERVAL`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``int`` | ++--------------+-------------+ +| **default**: | ``5`` | ++--------------+-------------+ + +This setting allows you to configure the config check interval used by +`config_applied <#configuration-applied>`_. By default it is set to 5 minutes. + +``OPENWISP_MONITORING_AUTO_IPERF3`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``False`` | ++--------------+-------------+ + +This setting allows you to choose whether `iperf3 <#iperf3-1>`_ checks should be +created automatically for newly registered devices. It's disabled by default. + +``OPENWISP_MONITORING_IPERF3_CHECK_CONFIG`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``dict`` | ++--------------+-------------+ +| **default**: | ``{}`` | ++--------------+-------------+ + +This setting allows to override the default iperf3 check configuration defined in +``openwisp_monitoring.check.classes.iperf3.DEFAULT_IPERF3_CHECK_CONFIG``. + +For example, you can change the values of `supported iperf3 check parameters <#iperf3-check-parameters>`_. + +.. code-block:: python + + OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { + # 'org_pk' : {'host' : [], 'client_options' : {}} + 'a9734710-db30-46b0-a2fc-01f01046fe4f': { + # Some public iperf3 servers + # https://iperf.fr/iperf-servers.php#public-servers + 'host': ['iperf3.openwisp.io', '2001:db8::1', '192.168.5.2'], + 'client_options': { + 'port': 6209, + # Number of parallel client streams to run + # note that iperf3 is single threaded + # so if you are CPU bound this will not + # yield higher throughput + 'parallel': 5, + # Set the connect_timeout (in milliseconds) for establishing + # the initial control connection to the server, the lower the value + # the faster the down iperf3 server will be detected (ex. 1000 ms (1 sec)) + 'connect_timeout': 1000, + # Window size / socket buffer size + 'window': '300K', + # Only one reverse condition can be chosen, + # reverse or bidirectional + 'reverse': True, + # Only one test end condition can be chosen, + # time, bytes or blockcount + 'blockcount': '1K', + 'udp': {'bitrate': '50M', 'length': '1460K'}, + 'tcp': {'bitrate': '20M', 'length': '256K'}, + }, + } + } + +``OPENWISP_MONITORING_IPERF3_CHECK_DELETE_RSA_KEY`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------------------------+ +| **type**: | ``bool`` | ++--------------+-------------------------------+ +| **default**: | ``True`` | ++--------------+-------------------------------+ + +This setting allows you to set whether +`iperf3 check RSA public key <#configure-iperf3-check-for-authentication>`_ +will be deleted after successful completion of the check or not. + +``OPENWISP_MONITORING_IPERF3_CHECK_LOCK_EXPIRE`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------------------------+ +| **type**: | ``int`` | ++--------------+-------------------------------+ +| **default**: | ``600`` | ++--------------+-------------------------------+ + +This setting allows you to set a cache lock expiration time for the iperf3 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_CHARTS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------------------------------------------------------------+ +| **type**: | ``list`` | ++--------------+-----------------------------------------------------------------+ +| **default**: | ``('traffic', 'wifi_clients', 'uptime', 'packet_loss', 'rtt')`` | ++--------------+-----------------------------------------------------------------+ + +Automatically created charts. + +``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------------------------------------------------------------+ +| **type**: | ``list`` of ``dict`` objects | ++--------------+-----------------------------------------------------------------+ +| **default**: | ``[{'key': 'ping', 'field_name': 'reachable'}]`` | ++--------------+-----------------------------------------------------------------+ + +Device metrics that are considered critical: + +when a value crosses the boundary defined in the "threshold value" field +of the alert settings related to one of these metric types, the health status +of the device related to the metric moves into ``CRITICAL``. + +By default, if devices are not reachable by pings they are flagged as ``CRITICAL``. + +``OPENWISP_MONITORING_HEALTH_STATUS_LABELS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------------------------------------------------------------------------+ +| **type**: | ``dict`` | ++--------------+--------------------------------------------------------------------------------------+ +| **default**: | ``{'unknown': 'unknown', 'ok': 'ok', 'problem': 'problem', 'critical': 'critical'}`` | ++--------------+--------------------------------------------------------------------------------------+ + +This setting allows to change the health status labels, for example, if we +want to use ``online`` instead of ``ok`` and ``offline`` instead of ``critical``, +you can use the following configuration: + +.. code-block:: python + + OPENWISP_MONITORING_HEALTH_STATUS_LABELS = { + 'ok': 'online', + 'problem': 'problem', + '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`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +By default, only the management IP will be used to perform active checks to +the devices. + +If the devices are connecting to your OpenWISP instance using a shared layer2 +network, hence the OpenWSP server can reach the devices using the ``last_ip`` +field, you can set this to ``False``. + +**Note:** If this setting is not configured, it will fallback to the value of +`OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting +`_. +If ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, +then it will fallback to ``True``. + +``OPENWISP_MONITORING_DEVICE_RECOVERY_DETECTION`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +When device recovery detection is enabled, recoveries are discovered as soon as +a device contacts the openwisp system again (eg: to get the configuration checksum +or to send monitoring metrics). + +This feature is enabled by default. + +If you use OpenVPN as the management VPN, you may want to check out a similar +integration built in **openwisp-network-topology**: when the status of an OpenVPN link +changes (detected by monitoring the status information of OpenVPN), the +network topology module will trigger the monitoring checks. +For more information see: +`Network Topology Device Integration `_ + +``OPENWISP_MONITORING_MAC_VENDOR_DETECTION`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Indicates whether mac addresses will be complemented with hardware vendor +information by performing lookups on the OUI +(Organization Unique Identifier) table. + +This feature is enabled by default. + +``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``dict`` | ++--------------+-----------+ +| **default**: | see below | ++--------------+-----------+ + +.. code-block:: python + + # default value of OPENWISP_MONITORING_RETRY_OPTIONS: + + dict( + max_retries=None, + retry_backoff=True, + retry_backoff_max=600, + retry_jitter=True, + ) + +Retry settings for recoverable failures during metric writes. + +By default if a metric write fails (eg: due to excessive load on timeseries database at that moment) +then the operation will be retried indefinitely with an exponential random backoff and a maximum delay of 10 minutes. + +This feature makes the monitoring system resilient to temporary outages and helps to prevent data loss. + +For more information regarding these settings, consult the `celery documentation +regarding automatic retries for known errors +`_. + +**Note:** The retry mechanism does not work when using ``UDP`` for writing +data to the timeseries database. It is due to the nature of ``UDP`` protocol +which does not acknowledge receipt of data packets. + +``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``dict`` | ++--------------+-----------+ +| **default**: | see below | ++--------------+-----------+ + +.. code-block:: python + + # default value of OPENWISP_MONITORING_RETRY_OPTIONS: + + dict( + max_retries=6, + delay=2 + ) + +On busy systems, communication with the timeseries DB can occasionally fail. +The timeseries DB backend will retry on any exception according to these settings. +The delay kicks in only after the third consecutive attempt. + +This setting shall not be confused with ``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS``, +which is used to configure the infinite retrying of the celery task which writes +metric data to the timeseries DB, while ``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` +deals with any other read/write operation on the timeseries DB which may fail. + +However these retries are not handled by celery but are simple python loops, +which will eventually give up if a problem persists. + +``OPENWISP_MONITORING_TIMESERIES_RETRY_DELAY`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``int`` | ++--------------+-------------+ +| **default**: | ``2`` | ++--------------+-------------+ + +This settings allow you to configure the retry delay time (in seconds) after 3 failed attempt in timeseries database. + +This retry setting is used in retry mechanism to make the requests to the timeseries database resilient. + +This setting is independent of celery retry settings. + +``OPENWISP_MONITORING_DASHBOARD_MAP`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Whether the geographic map in the dashboard is enabled or not. +This feature provides a geographic map which shows the locations +which have devices installed in and provides a visual representation +of the monitoring status of the devices, this allows to get +an overview of the network at glance. + +This feature is enabled by default and depends on the setting +``OPENWISP_ADMIN_DASHBOARD_ENABLED`` from +`openwisp-utils `__ +being set to ``True`` (which is the default). + +You can turn this off if you do not use the geographic features +of OpenWISP. + +``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------------------------------+ +| **type**: | ``dict`` | ++--------------+--------------------------------------------+ +| **default**: | ``{'__all__': ['wan', 'eth1', 'eth0.2']}`` | ++--------------+--------------------------------------------+ + +This settings allows to configure the interfaces which should +be included in the **General Traffic** chart in the admin dashboard. + +This setting should be defined in the following format: + +.. code-block::python + + OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART = { + '': [''] + } + +E.g., if you want the **General Traffic** chart to show data from +two interfaces for an organization, you need to configure this setting +as follows: + +.. code-block::python + + OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART = { + # organization uuid + 'f9601bbd-b6d5-4704-85e3-5851894437bf': ['eth1', 'eth2'] + } + +**Note**: The value of ``__all__`` key is used if an organization +does not have list of interfaces defined in ``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART``. + +**Note**: If a user can manage more than one organization (e.g. superusers), +then the **General Traffic** chart will always show data from interfaces +of ``__all__`` configuration. + +``OPENWISP_MONITORING_METRICS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``dict`` | ++--------------+-------------+ +| **default**: | ``{}`` | ++--------------+-------------+ + +This setting allows to define additional metric configuration or to override +the default metric configuration defined in +``openwisp_monitoring.monitoring.configuration.DEFAULT_METRICS``. + +For example, if you want to change only the **field_name** of +``clients`` metric to ``wifi_clients`` you can use: + +.. code-block:: python + + from django.utils.translation import gettext_lazy as _ + + OPENWISP_MONITORING_METRICS = { + 'clients': { + 'label': _('WiFi clients'), + 'field_name': 'wifi_clients', + }, + } + +For example, if you want to change only the default alert settings of +``memory`` metric you can use: + +.. code-block:: python + + OPENWISP_MONITORING_METRICS = { + 'memory': { + 'alert_settings': {'threshold': 75, 'tolerance': 10} + }, + } + +For example, if you want to change only the notification of +``config_applied`` metric you can use: + +.. code-block:: python + + from django.utils.translation import gettext_lazy as _ + + OPENWISP_MONITORING_METRICS = { + 'config_applied': { + 'notification': { + 'problem': { + 'verbose_name': 'Configuration PROBLEM', + 'verb': _('has not been applied'), + 'email_subject': _( + '[{site.name}] PROBLEM: {notification.target} configuration ' + 'status issue' + ), + 'message': _( + 'The configuration for device [{notification.target}]' + '({notification.target_link}) {notification.verb} in a timely manner.' + ), + }, + 'recovery': { + 'verbose_name': 'Configuration RECOVERY', + 'verb': _('configuration has been applied again'), + 'email_subject': _( + '[{site.name}] RECOVERY: {notification.target} {notification.verb} ' + 'successfully' + ), + 'message': _( + 'The device [{notification.target}]({notification.target_link}) ' + '{notification.verb} successfully.' + ), + }, + }, + }, + } + +Or if you want to define a new metric configuration, which you can then +call in your custom code (eg: a custom check class), you can do so as follows: + +.. code-block:: python + + from django.utils.translation import gettext_lazy as _ + + OPENWISP_MONITORING_METRICS = { + 'top_fields_mean': { + 'name': 'Top Fields Mean', + 'key': '{key}', + 'field_name': '{field_name}', + 'label': '_(Top fields mean)', + 'related_fields': ['field1', 'field2', 'field3'], + }, + } + +``OPENWISP_MONITORING_CHARTS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``dict`` | ++--------------+-------------+ +| **default**: | ``{}`` | ++--------------+-------------+ + +This setting allows to define additional charts or to override +the default chart configuration defined in +``openwisp_monitoring.monitoring.configuration.DEFAULT_CHARTS``. + +In the following example, we modify the description of the traffic chart: + +.. code-block:: python + + OPENWISP_MONITORING_CHARTS = { + 'traffic': { + 'description': ( + 'Network traffic, download and upload, measured on ' + 'the interface "{metric.key}", custom message here.' + ), + } + } + +Or if you want to define a new chart configuration, which you can then +call in your custom code (eg: a custom check class), you can do so as follows: + +.. code-block:: python + + from django.utils.translation import gettext_lazy as _ + + OPENWISP_MONITORING_CHARTS = { + 'ram': { + 'type': 'line', + 'title': 'RAM usage', + 'description': 'RAM usage', + 'unit': 'bytes', + 'order': 100, + 'query': { + 'influxdb': ( + "SELECT MEAN(total) AS total, MEAN(free) AS free, " + "MEAN(buffered) AS buffered FROM {key} WHERE time >= '{time}' AND " + "content_type = '{content_type}' AND object_id = '{object_id}' " + "GROUP BY time(1d)" + ) + }, + } + } + +In case you just want to change the colors used in a chart here's how to do it: + +.. code-block:: python + + OPENWISP_MONITORING_CHARTS = { + 'traffic': { + 'colors': ['#000000', '#cccccc', '#111111'] + } + } + +``OPENWISP_MONITORING_DEFAULT_CHART_TIME`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++---------------------+---------------------------------------------+ +| **type**: | ``str`` | ++---------------------+---------------------------------------------+ +| **default**: | ``7d`` | ++---------------------+---------------------------------------------+ +| **possible values** | ``1d``, ``3d``, ``7d``, ``30d`` or ``365d`` | ++---------------------+---------------------------------------------+ + +Allows to set the default time period of the time series charts. + +``OPENWISP_MONITORING_AUTO_CLEAR_MANAGEMENT_IP`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +This setting allows you to automatically clear management_ip of a device +when it goes offline. It is enabled by default. + +``OPENWISP_MONITORING_API_URLCONF`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``string`` | ++--------------+-------------+ +| **default**: | ``None`` | ++--------------+-------------+ + +Changes the urlconf option of django urls to point the monitoring API +urls to another installed module, example, ``myapp.urls``. +(Useful when you have a seperate API instance.) + +``OPENWISP_MONITORING_API_BASEURL`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``string`` | ++--------------+-------------+ +| **default**: | ``None`` | ++--------------+-------------+ + +If you have a seperate server for API of openwisp-monitoring on a different +domain, you can use this option to change the base of the url, this will +enable you to point all the API urls to your openwisp-monitoring API server's +domain, example: ``https://mymonitoring.myapp.com``. + +``OPENWISP_MONITORING_CACHE_TIMEOUT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------------------------------+ +| **type**: | ``int`` | ++--------------+----------------------------------+ +| **default**: | ``86400`` (24 hours in seconds) | ++--------------+----------------------------------+ + +This setting allows to configure timeout (in seconds) for monitoring data cache. diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst new file mode 100644 index 00000000..5ccaf904 --- /dev/null +++ b/docs/user/wifi-sessions.rst @@ -0,0 +1,53 @@ +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. + +You can also view open WiFi sessions of a device directly from the device's change admin +under the "WiFi Sessions" tab. + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-wifi-session-inline.png + :align: center + +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. From 2daa075f49c80aa5913f7142812621ab51dcad04 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 25 Apr 2024 16:09:59 +0530 Subject: [PATCH 02/42] [chores] Fixed URL mapping --- .../registering-unregistering-metric-configuration.rst | 2 +- docs/overview.rst | 2 +- docs/user/adding-checks-and-alertsettings.rst | 2 +- docs/user/quickstart.rst | 2 +- docs/user/settings.rst | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/developer/registering-unregistering-metric-configuration.rst b/docs/developer/registering-unregistering-metric-configuration.rst index 4e21a01a..83c14ba1 100644 --- a/docs/developer/registering-unregistering-metric-configuration.rst +++ b/docs/developer/registering-unregistering-metric-configuration.rst @@ -143,7 +143,7 @@ which allows ``AlertSettings`` to check the ``threshold`` on is already registered with same name (not to be confused with verbose_name). If you don't need to register a new metric but need to change a specific key of an -existing metric configuration, you can use `OPENWISP_MONITORING_METRICS <#openwisp_monitoring_metrics>`_. +existing metric configuration, you can use :ref:`OPENWISP_MONITORING_METRICS <#openwisp_monitoring_metrics>`_. ``unregister_metric`` ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/overview.rst b/docs/overview.rst index 3ac09acc..fd589892 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -67,7 +67,7 @@ Available Features there are also `two timeseries charts which show the total unique WiFI clients and the traffic flowing to the network `_, a geographic map is also available for those who use the geographic features of OpenWISP -* Possibility to configure additional `Metrics <#openwisp_monitoring_metrics>`_ and `Charts <#openwisp_monitoring_charts>`_ +* Possibility to configure additional :ref:`Metrics <#openwisp_monitoring_metrics>`_ and `Charts <#openwisp_monitoring_charts>`_ * Extensible active check system: it's possible to write additional checks that are run periodically using python classes * Extensible metrics and charts: it's possible to define new metrics and new charts diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst index ac7f5892..8870354f 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -9,7 +9,7 @@ To add a check, you just need to select an available **check type** as shown bel :align: center The following example shows how to use the -`OPENWISP_MONITORING_METRICS setting <#openwisp_monitoring_metrics>`_ +:ref:`OPENWISP_MONITORING_METRICS setting <#openwisp_monitoring_metrics>`_ to reconfigure the system for `iperf3 check <#iperf3-1>`_ to send an alert if the measured **TCP bandwidth** has been less than **10 Mbit/s** for more than **2 days**. diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index bbc453fb..bcb4a4ba 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -23,7 +23,7 @@ on your device. These packages collect and send the monitoring data from the device to OpenWISP Monitoring and -are required to collect `metrics <#openwisp_monitoring_metrics>`_ +are required to collect :ref:`metrics <#openwisp_monitoring_metrics>`_ like interface traffic, WiFi clients, CPU load, memory usage, etc. **Note**: if you are an existing user of *openwisp-monitoring* and are using diff --git a/docs/user/settings.rst b/docs/user/settings.rst index c5ce197e..1c05e277 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -531,6 +531,8 @@ does not have list of interfaces defined in ``OPENWISP_MONITORING_DASHBOARD_TRAF then the **General Traffic** chart will always show data from interfaces of ``__all__`` configuration. +.. _openwisp_monitoring_metrics: + ``OPENWISP_MONITORING_METRICS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From fadb14946832ab32bc2aab6be810c683bf73a821 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 25 Apr 2024 20:03:07 +0530 Subject: [PATCH 03/42] [chores] Fixed references --- docs/developer/installation.rst | 2 + docs/developer/management-commands.rst | 6 +- ...ring-unregistering-chart-configuration.rst | 2 +- ...ing-unregistering-metric-configuration.rst | 2 +- docs/developer/signals.rst | 82 +++++++++++++++++++ docs/overview.rst | 4 +- docs/user/adding-checks-and-alertsettings.rst | 6 +- docs/user/available-checks.rst | 22 ++--- docs/user/dashboard-monitoring-charts.rst | 4 +- docs/user/default-metrics.rst | 2 +- docs/user/iperf3-usage-instructions.rst | 24 +++--- docs/user/quickstart.rst | 2 +- docs/user/rest-api.rst | 6 +- docs/user/settings.rst | 20 ++++- docs/user/wifi-sessions.rst | 4 +- 15 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 docs/developer/signals.rst diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 8fb98d13..9c49682e 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -9,6 +9,8 @@ See: - `ansible-openwisp2 `_ - `docker-openwisp `_ +.. _setup-integrate-in-an-existing-django-project: + Install system dependencies ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/developer/management-commands.rst b/docs/developer/management-commands.rst index 5ea306a7..65ede924 100644 --- a/docs/developer/management-commands.rst +++ b/docs/developer/management-commands.rst @@ -1,12 +1,14 @@ Management commands ------------------- +.. _run_checks: + ``run_checks`` ~~~~~~~~~~~~~~ -This command will execute all the `available checks <#available-checks>`_ for all the devices. +This command will execute all the `available checks `_ for all the devices. By default checks are run periodically by *celery beat*. You can learn more -about this in `Setup <#setup-integrate-in-an-existing-django-project>`_. +about this in :ref:`Setup `. Example usage: diff --git a/docs/developer/registering-unregistering-chart-configuration.rst b/docs/developer/registering-unregistering-chart-configuration.rst index 53ec1423..022f2a0f 100644 --- a/docs/developer/registering-unregistering-chart-configuration.rst +++ b/docs/developer/registering-unregistering-chart-configuration.rst @@ -47,7 +47,7 @@ An example usage has been shown below. is already registered with same name (not to be confused with verbose_name). If you don't need to register a new chart but need to change a specific key of an -existing chart configuration, you can use `OPENWISP_MONITORING_CHARTS <#openwisp_monitoring_charts>`_. +existing chart configuration, you can use :ref:`OPENWISP_MONITORING_CHARTS `. ``unregister_chart`` ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/developer/registering-unregistering-metric-configuration.rst b/docs/developer/registering-unregistering-metric-configuration.rst index 83c14ba1..395aa9f5 100644 --- a/docs/developer/registering-unregistering-metric-configuration.rst +++ b/docs/developer/registering-unregistering-metric-configuration.rst @@ -143,7 +143,7 @@ which allows ``AlertSettings`` to check the ``threshold`` on is already registered with same name (not to be confused with verbose_name). If you don't need to register a new metric but need to change a specific key of an -existing metric configuration, you can use :ref:`OPENWISP_MONITORING_METRICS <#openwisp_monitoring_metrics>`_. +existing metric configuration, you can use :ref:`OPENWISP_MONITORING_METRICS `. ``unregister_metric`` ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/developer/signals.rst b/docs/developer/signals.rst new file mode 100644 index 00000000..c811e5db --- /dev/null +++ b/docs/developer/signals.rst @@ -0,0 +1,82 @@ +Signals +------- + +``device_metrics_received`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.device.signals.device_metrics_received`` + +**Arguments**: + +- ``instance``: instance of ``Device`` whose metrics have been received +- ``request``: the HTTP request object +- ``time``: time with which metrics will be saved. If none, then server time will be used +- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues + +This signal is emitted when device metrics are received to the ``DeviceMetric`` +view (only when using HTTP POST). + +The signal is emitted just before a successful response is returned, +it is not sent if the response was not successful. + +``health_status_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.device.signals.health_status_changed`` + +**Arguments**: + +- ``instance``: instance of ``DeviceMonitoring`` whose status has been changed +- ``status``: the status by which DeviceMonitoring's existing status has been updated with + +This signal is emitted only if the health status of DeviceMonitoring object gets updated. + +``threshold_crossed`` +~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.signals.threshold_crossed`` + +**Arguments**: + +- ``metric``: ``Metric`` object whose threshold defined in related alert settings was crossed +- ``alert_settings``: ``AlertSettings`` related to the ``Metric`` +- ``target``: related ``Device`` object +- ``first_time``: it will be set to true when the metric is written for the first time. It shall be set to false afterwards. +- ``tolerance_crossed``: it will be set to true if the metric has crossed the threshold for tolerance configured in alert settings. + Otherwise, it will be set to false. + +``first_time`` parameter can be used to avoid initiating unneeded actions. +For example, sending recovery notifications. + +This signal is emitted when the threshold value of a ``Metric`` defined in +alert settings is crossed. + +``pre_metric_write`` +~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.signals.pre_metric_write`` + +**Arguments**: + +- ``metric``: ``Metric`` object whose data shall be stored in timeseries database +- ``values``: metric data that shall be stored in the timeseries database +- ``time``: time with which metrics will be saved +- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues + +This signal is emitted for every metric before the write operation is sent to +the timeseries database. + +``post_metric_write`` +~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.signals.post_metric_write`` + +**Arguments**: + +- ``metric``: ``Metric`` object whose data is being stored in timeseries database +- ``values``: metric data that is being stored in the timeseries database +- ``time``: time with which metrics will be saved +- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues + +This signal is emitted for every metric after the write operation is successfully +executed in the background. diff --git a/docs/overview.rst b/docs/overview.rst index fd589892..c9b75120 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -67,13 +67,13 @@ Available Features there are also `two timeseries charts which show the total unique WiFI clients and the traffic flowing to the network `_, a geographic map is also available for those who use the geographic features of OpenWISP -* Possibility to configure additional :ref:`Metrics <#openwisp_monitoring_metrics>`_ and `Charts <#openwisp_monitoring_charts>`_ +* Possibility to configure additional :ref:`Metrics ` and :ref:`Charts ` * Extensible active check system: it's possible to write additional checks that are run periodically using python classes * Extensible metrics and charts: it's possible to define new metrics and new charts * API to retrieve the chart metrics and status information of each device based on `NetJSON DeviceMonitoring `_ -* `Iperf3 check <#iperf3-1>`_ that provides network performance measurements such as maximum +* :ref:`Iperf3 check ` that provides network performance measurements such as maximum achievable bandwidth, jitter, datagram loss etc of the openwrt device using `iperf3 utility `_ .. toctree:: diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst index 8870354f..af030435 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -9,11 +9,11 @@ To add a check, you just need to select an available **check type** as shown bel :align: center The following example shows how to use the -:ref:`OPENWISP_MONITORING_METRICS setting <#openwisp_monitoring_metrics>`_ -to reconfigure the system for `iperf3 check <#iperf3-1>`_ to send an alert if +:ref:`OPENWISP_MONITORING_METRICS setting ` +to reconfigure the system for :ref:`iperf3 check ` to send an alert if the measured **TCP bandwidth** has been less than **10 Mbit/s** for more than **2 days**. -1. By default, `Iperf3 checks <#iperf3-1>`_ come with default alert settings, +1. By default, :ref:`Iperf3 checks ` come with default alert settings, but it is easy to customize alert settings through the device page as shown below: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-alertsettings.png diff --git a/docs/user/available-checks.rst b/docs/user/available-checks.rst index 9c875344..1a5bd4b4 100644 --- a/docs/user/available-checks.rst +++ b/docs/user/available-checks.rst @@ -8,10 +8,10 @@ This check returns information on device ``uptime`` and ``RTT (Round trip time)` The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. The ``fping`` command is used to collect these metrics. You may choose to disable auto creation of this check by setting -`OPENWISP_MONITORING_AUTO_PING <#OPENWISP_MONITORING_AUTO_PING>`_ to ``False``. +:ref:`OPENWISP_MONITORING_AUTO_PING ` to ``False``. You can change the default values used for ping checks using -`OPENWISP_MONITORING_PING_CHECK_CONFIG <#OPENWISP_MONITORING_PING_CHECK_CONFIG>`_ setting. +:ref:`OPENWISP_MONITORING_PING_CHECK_CONFIG ` setting. Configuration applied ~~~~~~~~~~~~~~~~~~~~~ @@ -19,13 +19,15 @@ Configuration applied This check ensures that the `openwisp-config agent `_ is running and applying configuration changes in a timely manner. You may choose to disable auto creation of this check by using the -setting `OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK <#OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK>`_. +setting :ref:`OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK `. This check runs periodically, but it is also triggered whenever the configuration status of a device changes, this ensures the check reacts quickly to events happening in the network and informs the user promptly if there's anything that is not working as intended. +.. _iperf3-1: + Iperf3 ~~~~~~ @@ -33,17 +35,17 @@ This check provides network performance measurements such as maximum achievable jitter, datagram loss etc of the device using `iperf3 utility `_. This check is **disabled by default**. You can enable auto creation of this check by setting the -`OPENWISP_MONITORING_AUTO_IPERF3 <#OPENWISP_MONITORING_AUTO_IPERF3>`_ to ``True``. +:ref:`OPENWISP_MONITORING_AUTO_IPERF3 ` to ``True``. -You can also `add the iperf3 check -<#add-checks-and-alert-settings-from-the-device-page>`_ directly from the device page. +You can also :ref:`add the iperf3 check +`_ directly from the device page. It also supports tuning of various parameters. You can also change the parameters used for iperf3 checks (e.g. timing, port, username, -password, rsa_publc_key etc) using the `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ setting. +password, rsa_publc_key etc) using the :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +<#openwisp_monitoring_iperf3_check_config>`_ setting. -**Note:** When setting `OPENWISP_MONITORING_AUTO_IPERF3 <#OPENWISP_MONITORING_AUTO_IPERF3>`_ to ``True``, -you may need to update the `metric configuration <#add-checks-and-alert-settings-from-the-device-page>`_ +**Note:** When setting :ref:`OPENWISP_MONITORING_AUTO_IPERF3 ` to ``True``, +you may need to update the :ref:`metric configuration ` to enable alerts for the iperf3 check. diff --git a/docs/user/dashboard-monitoring-charts.rst b/docs/user/dashboard-monitoring-charts.rst index ca0ea0d4..c890f6be 100644 --- a/docs/user/dashboard-monitoring-charts.rst +++ b/docs/user/dashboard-monitoring-charts.rst @@ -1,7 +1,7 @@ Dashboard Monitoring Charts --------------------------- -.. figure:: https://github.com/openwisp/openwisp-monitoring/blob/docs/docs/1.1/dashboard-charts.png +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/dashboard-charts.png :align: center OpenWISP Monitoring adds two timeseries charts to the admin dashboard: @@ -11,5 +11,5 @@ OpenWISP Monitoring adds two timeseries charts to the admin dashboard: - **General traffic Chart**: Shows the amount of traffic flowing in the network. You can configure the interfaces included in the **General traffic chart** using -the `"OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART" +the :ref:`"OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART" <#openwisp_monitoring_dashboard_traffic_chart>`_ setting. diff --git a/docs/user/default-metrics.rst b/docs/user/default-metrics.rst index 8558b3c7..cba6abf6 100644 --- a/docs/user/default-metrics.rst +++ b/docs/user/default-metrics.rst @@ -264,4 +264,4 @@ For more info on how to configure and use Iperf3, please refer to `iperf3 check usage instructions <#iperf3-check-usage-instructions>`_. **Note:** Iperf3 charts uses ``connect_points=True`` in -`default chart configuration <#openwisp_monitoring_charts>`_ that joins it's individual chart data points. +:ref:`default chart configuration ` that joins it's individual chart data points. diff --git a/docs/user/iperf3-usage-instructions.rst b/docs/user/iperf3-usage-instructions.rst index 71757fd4..e46a77d9 100644 --- a/docs/user/iperf3-usage-instructions.rst +++ b/docs/user/iperf3-usage-instructions.rst @@ -24,9 +24,9 @@ to allow SSH access to you device from OpenWISP. **Note:** Make sure device connection is enabled & working with right update strategy i.e. ``OpenWRT SSH``. -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png - :alt: Enable ssh access from openwisp to device - :align: center +.. .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png +.. :alt: Enable ssh access from openwisp to device +.. :align: center 3. Set up and configure Iperf3 server settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -103,12 +103,12 @@ traffic times to not interfere with standard traffic. This should happen automatically if you have celery-beat correctly configured and running in the background. For testing purposes, you can run this check manually using the -`run_checks <#run_checks>`_ command. +:ref:`run_checks ` command. After that, you should see the iperf3 network measurements charts. -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png - :alt: Iperf3 network measurement charts +.. .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png +.. :alt: Iperf3 network measurement charts Iperf3 check parameters ~~~~~~~~~~~~~~~~~~~~~~~ @@ -166,7 +166,7 @@ Currently, iperf3 check supports the following parameters: +-----------------------+-------------------------------------------------------------------------------+ To learn how to use these parameters, please see the -`iperf3 check configuration example <#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_. +:ref:`iperf3 check configuration example `. Visit the `official documentation `_ to learn more about the iperf3 parameters. @@ -193,8 +193,8 @@ Server side After running the commands mentioned above, the public key will be stored in ``public_key.pem`` which will be used in **rsa_public_key** parameter -in `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ +in :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +` and the private key will be contained in the file ``private_key.pem`` which will be used with **--rsa-private-key-path** command option when starting the iperf3 server. @@ -245,12 +245,14 @@ You may also check your installed **iperf3 openwrt package** features: Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, sendfile / zerocopy, socket pacing, authentication # contains 'authentication' +.. _configure-iperf3-check-auth-parameters: + 2. Configure iperf3 check auth parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now, add the following iperf3 authentication parameters -to `OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#OPENWISP_MONITORING_IPERF3_CHECK_CONFIG>`_ +to :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +` in the settings: .. code-block:: python diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index bcb4a4ba..05b372a2 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -23,7 +23,7 @@ on your device. These packages collect and send the monitoring data from the device to OpenWISP Monitoring and -are required to collect :ref:`metrics <#openwisp_monitoring_metrics>`_ +are required to collect :ref:`metrics ` like interface traffic, WiFi clients, CPU load, memory usage, etc. **Note**: if you are an existing user of *openwisp-monitoring* and are using diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index d99b9894..4fef3653 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -4,15 +4,15 @@ Rest API Live documentation ~~~~~~~~~~~~~~~~~~ -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png +image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. Browsable web interface ~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png +image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png +image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png Additionally, opening any of the endpoints `listed below <#list-of-endpoints>`_ directly in the browser will show the `browsable API interface of Django-REST-Framework diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 1c05e277..7f2d2387 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -108,6 +108,8 @@ This data is only used to assess the recent status of devices, keeping it for a long time would not add much benefit and would cost a lot more in terms of disk space. +.. _openwisp_monitoring_auto_ping: + ``OPENWISP_MONITORING_AUTO_PING`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -119,6 +121,8 @@ in terms of disk space. Whether ping checks are created automatically for devices. +.. _openwisp_monitoring_ping_check_config: + ``OPENWISP_MONITORING_PING_CHECK_CONFIG`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -161,6 +165,8 @@ as following: **Note:** Above ``maximum`` and ``minimum`` values are only used for validating custom parameters of a ``Check`` object. +.. _openwisp_monitoring_auto_device_config_check: + ``OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -185,6 +191,8 @@ created automatically for newly registered devices. It's enabled by default. This setting allows you to configure the config check interval used by `config_applied <#configuration-applied>`_. By default it is set to 5 minutes. +.. _openwisp_monitoring_auto_iperf3: + ``OPENWISP_MONITORING_AUTO_IPERF3`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -194,9 +202,11 @@ This setting allows you to configure the config check interval used by | **default**: | ``False`` | +--------------+-------------+ -This setting allows you to choose whether `iperf3 <#iperf3-1>`_ checks should be +This setting allows you to choose whether :ref:`iperf3 ` checks should be created automatically for newly registered devices. It's disabled by default. +.. _openwisp_monitoring_iperf3_check_config: + ``OPENWISP_MONITORING_IPERF3_CHECK_CONFIG`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -254,7 +264,7 @@ For example, you can change the values of `supported iperf3 check parameters <#i +--------------+-------------------------------+ This setting allows you to set whether -`iperf3 check RSA public key <#configure-iperf3-check-for-authentication>`_ +:ref:`iperf3 check RSA public key ` will be deleted after successful completion of the check or not. ``OPENWISP_MONITORING_IPERF3_CHECK_LOCK_EXPIRE`` @@ -319,6 +329,8 @@ you can use the following configuration: 'critical': 'offline' } +.. _openwisp_monitoring_wifi_sessions_enabled: + ``OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -493,6 +505,8 @@ being set to ``True`` (which is the default). You can turn this off if you do not use the geographic features of OpenWISP. +.. _openwisp_monitoring_dashboard_traffic_chart: + ``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -626,6 +640,8 @@ call in your custom code (eg: a custom check class), you can do so as follows: }, } +.. _openwisp_monitoring_charts: + ``OPENWISP_MONITORING_CHARTS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 5ccaf904..3104dcd9 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -16,7 +16,7 @@ are connected to or even directly by a *device* name or ID. :align: center You can disable this feature by configuring -`OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED <#openwisp_monitoring_wifi_sessions_enabled>`_ +:ref:`OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED ` setting. You can also view open WiFi sessions of a device directly from the device's change admin @@ -31,7 +31,7 @@ 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>`_. +in :ref:`setup instructions `. 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. From 67760558d5a45af59179a9ea7c7b18205b5f6e0a Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 25 Apr 2024 22:26:55 +0530 Subject: [PATCH 04/42] [chores] Fixed images --- docs/user/iperf3-usage-instructions.rst | 10 +++++----- docs/user/rest-api.rst | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/user/iperf3-usage-instructions.rst b/docs/user/iperf3-usage-instructions.rst index e46a77d9..bef6395e 100644 --- a/docs/user/iperf3-usage-instructions.rst +++ b/docs/user/iperf3-usage-instructions.rst @@ -24,9 +24,9 @@ to allow SSH access to you device from OpenWISP. **Note:** Make sure device connection is enabled & working with right update strategy i.e. ``OpenWRT SSH``. -.. .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png -.. :alt: Enable ssh access from openwisp to device -.. :align: center +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png + :alt: Enable ssh access from openwisp to device + :align: center 3. Set up and configure Iperf3 server settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -107,8 +107,8 @@ For testing purposes, you can run this check manually using the After that, you should see the iperf3 network measurements charts. -.. .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png -.. :alt: Iperf3 network measurement charts +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png + :alt: Iperf3 network measurement charts Iperf3 check parameters ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 4fef3653..d99b9894 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -4,15 +4,15 @@ Rest API Live documentation ~~~~~~~~~~~~~~~~~~ -image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. Browsable web interface ~~~~~~~~~~~~~~~~~~~~~~~ -image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png -image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png +.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png Additionally, opening any of the endpoints `listed below <#list-of-endpoints>`_ directly in the browser will show the `browsable API interface of Django-REST-Framework From 2ec3f9727639d738fe94563cb9e4ad745d3e4e43 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 26 Apr 2024 13:24:20 +0530 Subject: [PATCH 05/42] [chores] Improved references --- docs/overview.rst | 3 +-- docs/user/available-checks.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/overview.rst b/docs/overview.rst index c9b75120..a46a9903 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -38,8 +38,7 @@ see the .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png :align: center -Available Features ------------------- +**Available Features** * Collection of monitoring information in a timeseries database (currently only influxdb is supported) * Allows to browse alerts easily from the user interface with one click diff --git a/docs/user/available-checks.rst b/docs/user/available-checks.rst index 1a5bd4b4..00e2d256 100644 --- a/docs/user/available-checks.rst +++ b/docs/user/available-checks.rst @@ -47,5 +47,5 @@ password, rsa_publc_key etc) using the :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CO <#openwisp_monitoring_iperf3_check_config>`_ setting. **Note:** When setting :ref:`OPENWISP_MONITORING_AUTO_IPERF3 ` to ``True``, -you may need to update the :ref:`metric configuration ` +you may need to update the :ref:`metric configuration ` to enable alerts for the iperf3 check. From 4a1de54caea97ff09a3c41ddc803bfe99572a951 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 26 Apr 2024 17:23:50 +0530 Subject: [PATCH 06/42] [chores] Added developer docs warning to all developer pages --- docs/developer/developer-docs.rst | 2 +- docs/developer/exceptions.rst | 2 ++ docs/developer/extending.rst | 2 ++ docs/developer/installation.rst | 2 ++ docs/developer/management-commands.rst | 2 ++ docs/developer/monitoring-scripts.rst | 2 ++ docs/developer/registering-new-notification-types.rst | 2 ++ .../developer/registering-unregistering-chart-configuration.rst | 2 ++ .../registering-unregistering-metric-configuration.rst | 2 ++ docs/developer/signals.rst | 2 ++ 10 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst index 49045ed4..d645d2ac 100644 --- a/docs/developer/developer-docs.rst +++ b/docs/developer/developer-docs.rst @@ -1,7 +1,7 @@ Developers Documentation ------------------------ -.. include:: /paritals/developers-docs-warning.rst +.. include:: /partials/developers-docs-warning.rst .. toctree:: :maxdepth: 1 diff --git a/docs/developer/exceptions.rst b/docs/developer/exceptions.rst index bef66e52..271a0f78 100644 --- a/docs/developer/exceptions.rst +++ b/docs/developer/exceptions.rst @@ -1,6 +1,8 @@ Exceptions ---------- +.. include:: /partials/developers-docs-warning.rst + ``TimeseriesWriteException`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 17829bf9..efa5bc16 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,6 +1,8 @@ Extending openwisp-monitoring ----------------------------- +.. include:: /partials/developers-docs-warning.rst + One of the core values of the OpenWISP project is `Software Reusability `_, for this reason *openwisp-monitoring* provides a set of base classes which can be imported, extended and reused to create derivative apps. diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 9c49682e..8cd6dd73 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,6 +1,8 @@ Installation instructions ------------------------- +.. include:: /partials/developers-docs-warning.rst + Deploy it in production ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/developer/management-commands.rst b/docs/developer/management-commands.rst index 65ede924..0a861844 100644 --- a/docs/developer/management-commands.rst +++ b/docs/developer/management-commands.rst @@ -1,6 +1,8 @@ Management commands ------------------- +.. include:: /partials/developers-docs-warning.rst + .. _run_checks: ``run_checks`` diff --git a/docs/developer/monitoring-scripts.rst b/docs/developer/monitoring-scripts.rst index e40c708d..ab4f1cf7 100644 --- a/docs/developer/monitoring-scripts.rst +++ b/docs/developer/monitoring-scripts.rst @@ -1,6 +1,8 @@ Monitoring scripts ------------------ +.. include:: /partials/developers-docs-warning.rst + Monitoring scripts are now deprecated in favour of `monitoring packages `_. Follow the migration guide in `Migrating from monitoring scripts to monitoring packages <#migrating-from-monitoring-scripts-to-monitoring-packages>`_ section of this documentation. diff --git a/docs/developer/registering-new-notification-types.rst b/docs/developer/registering-new-notification-types.rst index 9bd57380..06e7ed98 100644 --- a/docs/developer/registering-new-notification-types.rst +++ b/docs/developer/registering-new-notification-types.rst @@ -1,6 +1,8 @@ Registering new notification types ---------------------------------- +.. include:: /partials/developers-docs-warning.rst + You can define your own notification types using ``register_notification_type`` function from OpenWISP Notifications. For more information, see the relevant `openwisp-notifications section about registering notification types `_. diff --git a/docs/developer/registering-unregistering-chart-configuration.rst b/docs/developer/registering-unregistering-chart-configuration.rst index 022f2a0f..f02963ce 100644 --- a/docs/developer/registering-unregistering-chart-configuration.rst +++ b/docs/developer/registering-unregistering-chart-configuration.rst @@ -1,6 +1,8 @@ Registering / Unregistering Chart Configuration ----------------------------------------------- +.. include:: /partials/developers-docs-warning.rst + **OpenWISP Monitoring** provides registering and unregistering chart configuration through utility functions ``openwisp_monitoring.monitoring.configuration.register_chart`` and ``openwisp_monitoring.monitoring.configuration.unregister_chart``. Using these functions you can register or unregister chart configurations from anywhere in your code. diff --git a/docs/developer/registering-unregistering-metric-configuration.rst b/docs/developer/registering-unregistering-metric-configuration.rst index 395aa9f5..b874756f 100644 --- a/docs/developer/registering-unregistering-metric-configuration.rst +++ b/docs/developer/registering-unregistering-metric-configuration.rst @@ -1,6 +1,8 @@ Registering / Unregistering Metric Configuration ------------------------------------------------ +.. include:: /partials/developers-docs-warning.rst + **OpenWISP Monitoring** provides registering and unregistering metric configuration through utility functions ``openwisp_monitoring.monitoring.configuration.register_metric`` and ``openwisp_monitoring.monitoring.configuration.unregister_metric``. Using these functions you can register or unregister metric configurations from anywhere in your code. diff --git a/docs/developer/signals.rst b/docs/developer/signals.rst index c811e5db..673b19d2 100644 --- a/docs/developer/signals.rst +++ b/docs/developer/signals.rst @@ -1,6 +1,8 @@ Signals ------- +.. include:: /partials/developers-docs-warning.rst + ``device_metrics_received`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 782b61536f09e0f395c1c2c18be76b2cf9f122ad Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 15 May 2024 21:04:51 -0400 Subject: [PATCH 07/42] [docs] Renamed overview to index --- docs/{overview.rst => index.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{overview.rst => index.rst} (100%) diff --git a/docs/overview.rst b/docs/index.rst similarity index 100% rename from docs/overview.rst rename to docs/index.rst From 2420738a622edbcf92195bd8f5d14d881dbe0abc Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Tue, 28 May 2024 00:30:15 +0530 Subject: [PATCH 08/42] [qa] Formatted with docstrfmt docstrfmt -l 74 --- CHANGES.rst | 138 +-- CONTRIBUTING.rst | 3 +- README.rst | 168 ++-- docs/developer/developer-docs.rst | 22 +- docs/developer/exceptions.rst | 27 +- docs/developer/extending.rst | 390 +++++--- docs/developer/installation.rst | 53 +- docs/developer/management-commands.rst | 13 +- docs/developer/monitoring-scripts.rst | 63 +- .../registering-new-notification-types.rst | 13 +- ...ring-unregistering-chart-configuration.rst | 79 +- ...ing-unregistering-metric-configuration.rst | 240 ++--- docs/developer/signals.rst | 66 +- docs/index.rst | 160 +-- docs/user/adaptive-size-charts.rst | 41 +- docs/user/adding-checks-and-alertsettings.rst | 107 ++- docs/user/available-checks.rst | 62 +- docs/user/dashboard-monitoring-charts.rst | 16 +- .../user/default-alerts-and-notifications.rst | 30 +- docs/user/default-metrics.rst | 331 +++---- docs/user/device-health-status.rst | 35 +- docs/user/iperf3-usage-instructions.rst | 363 +++---- .../passive-vs-active-metric-collection.rst | 19 +- docs/user/quickstart.rst | 128 +-- docs/user/rest-api.rst | 217 +++-- docs/user/settings.rst | 907 +++++++++--------- docs/user/wifi-sessions.rst | 49 +- 27 files changed, 1975 insertions(+), 1765 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e10123e1..cd259b82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,11 +12,10 @@ Version 1.0.3 [2022-12-29] Bugfixes ~~~~~~~~ -- Fixed data collection for missing mobile signal: - Skip writing mobile signal metric if mobile signal - info is missing. -- Fixed device health status changing to ``problem`` - when the configuration status changes to ``modified``. +- Fixed data collection for missing mobile signal: Skip writing mobile + signal metric if mobile signal info is missing. +- Fixed device health status changing to ``problem`` when the + configuration status changes to ``modified``. Version 1.0.2 [2022-08-04] -------------------------- @@ -24,10 +23,9 @@ Version 1.0.2 [2022-08-04] Bugfixes ~~~~~~~~ -- Fixed migrations which create checks for existing devices; - this problem was happening to OpenWISP instances which were - deployed without OpenWISP Monitoring and then enabled - the monitoring features +- Fixed migrations which create checks for existing devices; this problem + was happening to OpenWISP instances which were deployed without OpenWISP + Monitoring and then enabled the monitoring features Version 1.0.1 [2022-07-01] -------------------------- @@ -35,11 +33,10 @@ Version 1.0.1 [2022-07-01] Bugfixes ~~~~~~~~ -- Removed hardcoded static URLs which created - issues when static files are served using an - external service (e.g. S3 storage buckets) -- Fixed `"migrate_timeseries" command stalling - when measurements exceeds retention policy +- Removed hardcoded static URLs which created issues when static files are + served using an external service (e.g. S3 storage buckets) +- Fixed `"migrate_timeseries" command stalling when measurements exceeds + retention policy `_ Version 1.0.0 [2022-05-05] @@ -48,17 +45,19 @@ Version 1.0.0 [2022-05-05] Features ~~~~~~~~ -- Added metrics for mobile (5G/LTE/UMTS/GSM) - `signal strength `_, - `signal quality `_ +- Added metrics for mobile (5G/LTE/UMTS/GSM) `signal strength + `_, + `signal quality + `_ and `mobile access technology in use `_. -- Made `Ping check configurable `_ -- Added monitoring status chart to the dashboard and - a geographic map which shows a visual representation of the - monitoring the status of the devices. -- Added functionality to automatically clear the device's ``management_ip`` - when a device goes offline +- Made `Ping check configurable + `_ +- Added monitoring status chart to the dashboard and a geographic map + which shows a visual representation of the monitoring the status of the + devices. +- Added functionality to automatically clear the device's + ``management_ip`` when a device goes offline - Added support for specifying the time for received time-series data. - Made read requests to timeseries DB resilient to failures @@ -66,33 +65,41 @@ Changes ~~~~~~~ Backward incompatible changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++++++++++++++++++++++++++++++ -- *Monitoring Template* is removed in favour of - `openwisp monitoring packages `_. +- *Monitoring Template* is removed in favour of `openwisp monitoring + packages + `_. Follow the migration guide in `migrating from monitoring scripts to - monitoring packages `_ + monitoring packages + `_ section of openwisp-monitoring documentation. - If you have made changes to the default *Monitoring Template*, then create a backup of your template before running migrations. Running migrations will make changes to the default *Monitoring Template*. -- The time-series database schema for storing - `interface traffic `_ - and `associated WiFi clients `_ - has been updated. The data for *interface traffic* and *associated WiFi clients* - is stored in ``traffic`` and ``wifi_clients`` measurements respectively. - The Django migrations will perform the necessary operations in the time-series - database aysnchronously. It is recommended that you backup the time-series - database before running the migrations. - - You can use the `migrate_timeseries `_ +- The time-series database schema for storing `interface traffic + `_ and + `associated WiFi clients + `_ has + been updated. The data for *interface traffic* and *associated WiFi + clients* is stored in ``traffic`` and ``wifi_clients`` measurements + respectively. The Django migrations will perform the necessary + operations in the time-series database aysnchronously. It is recommended + that you backup the time-series database before running the migrations. + + You can use the `migrate_timeseries + `_ management command to trigger the migration of the time-series database. -- The `interface traffic `_ - and `associated WiFi clients `_ - metrics store additional tags, i.e. ``organization_id``, ``location_id`` and ``floorplan_id``. + +- The `interface traffic + `_ and + `associated WiFi clients + `_ metrics + store additional tags, i.e. ``organization_id``, ``location_id`` and + ``floorplan_id``. Dependencies -^^^^^^^^^^^^ +++++++++++++ - Dropped support for Python 3.6 - Dropped support for Django 2.2 @@ -104,37 +111,38 @@ Dependencies - Upgraded django-nested-admin to 3.4.0 Other changes -^^^^^^^^^^^^^ - -- *Configuration applied* check is triggered whenever the - configuration status of a device changes -- Added a default ``5`` minutes tolerance to ``CPU`` and ``memory`` - alert settings. -- Increased threshold value for ``disk`` alert settings from - *80%* to *90%*, since some device models have limited flash and - would trigger the alert in many cases. ++++++++++++++ + +- *Configuration applied* check is triggered whenever the configuration + status of a device changes +- Added a default ``5`` minutes tolerance to ``CPU`` and ``memory`` alert + settings. +- Increased threshold value for ``disk`` alert settings from *80%* to + *90%*, since some device models have limited flash and would trigger the + alert in many cases. - Renamed ``Check.check`` field to ``Check.check_type`` -- Made metric health status independent of AlertSetting tolerance. - Added ``tolerance_crossed`` parameter in +- Made metric health status independent of AlertSetting tolerance. Added + ``tolerance_crossed`` parameter in ``openwisp_monitoring.monitoring.signals.threshold_crossed`` signal -- The system does not sends connection notifications if the - connectivity of the device changes -- Improved UX of device's reachability (ping) chart. - Added more colours to represent different scenarios -- Avoid showing charts which have empty data in the REST API response - and in the device charts admin page +- The system does not sends connection notifications if the connectivity + of the device changes +- Improved UX of device's reachability (ping) chart. Added more colours to + represent different scenarios +- Avoid showing charts which have empty data in the REST API response and + in the device charts admin page Bugfixes ~~~~~~~~ -- Fixed a bug that caused inconsistency in the order of chart summary values +- Fixed a bug that caused inconsistency in the order of chart summary + values - Fixed bugs in restoring deleted devices using ``django-reversion`` -- Fixed migrations referencing non-swappable OpenWISP modules - that broke OpenWISP's extensibility -- Skip retry for writing metrics beyond retention policy. - The celery worker kept on retrying writing data to InfluxDB even - when the data points crossed the retention policy of InfluxDB. This - led to accumulation of such tasks which overloaded the celery workers. +- Fixed migrations referencing non-swappable OpenWISP modules that broke + OpenWISP's extensibility +- Skip retry for writing metrics beyond retention policy. The celery + worker kept on retrying writing data to InfluxDB even when the data + points crossed the retention policy of InfluxDB. This led to + accumulation of such tasks which overloaded the celery workers. Version 0.1.0 [2021-01-31] -------------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f74a12db..f691ec27 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1 +1,2 @@ -Please refer to the `Contribution Guidelines `_. +Please refer to the `Contribution Guidelines +`_. diff --git a/README.rst b/README.rst index 53967201..8480d43c 100644 --- a/README.rst +++ b/README.rst @@ -10,117 +10,131 @@ openwisp-monitoring :alt: Test coverage .. image:: https://img.shields.io/librariesio/github/openwisp/openwisp-monitoring - :target: https://libraries.io/github/openwisp/openwisp-monitoring#repository_dependencies - :alt: Dependency monitoring + :target: https://libraries.io/github/openwisp/openwisp-monitoring#repository_dependencies + :alt: Dependency monitoring .. image:: https://badge.fury.io/py/openwisp-monitoring.svg :target: http://badge.fury.io/py/openwisp-monitoring :alt: pypi .. image:: https://pepy.tech/badge/openwisp-monitoring - :target: https://pepy.tech/project/openwisp-monitoring - :alt: downloads + :target: https://pepy.tech/project/openwisp-monitoring + :alt: downloads .. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square - :target: https://gitter.im/openwisp/monitoring - :alt: support chat + :target: https://gitter.im/openwisp/monitoring + :alt: support chat .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://pypi.org/project/black/ - :alt: code style: black + :target: https://pypi.org/project/black/ + :alt: code style: black .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/monitoring-demo.gif - :target: https://github.com/openwisp/openwisp-monitoring/tree/docs/docs/monitoring-demo.gif - :alt: Feature Highlights + :target: https://github.com/openwisp/openwisp-monitoring/tree/docs/docs/monitoring-demo.gif + :alt: Feature Highlights ------------- +---- -**Need a quick overview?** `Try the OpenWISP Demo `_. +**Need a quick overview?** `Try the OpenWISP Demo +`_. -OpenWISP Monitoring is a network monitoring system written in Python and Django, -designed to be **extensible**, **programmable**, **scalable** and easy to use by end users: -once the system is configured, monitoring checks, alerts and metric collection -happens automatically. +OpenWISP Monitoring is a network monitoring system written in Python and +Django, designed to be **extensible**, **programmable**, **scalable** and +easy to use by end users: once the system is configured, monitoring +checks, alerts and metric collection happens automatically. See the `available features <#available-features>`_. -`OpenWISP `_ is not only an application designed for end users, -but can also be used as a framework on which custom network automation solutions can be -built on top of its building blocks. +`OpenWISP `_ is not only an application designed for +end users, but can also be used as a framework on which custom network +automation solutions can be built on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: -- `openwisp-controller `_: - network and WiFi controller: provisioning, configuration management, - x509 PKI management and more; works on OpenWRT, but designed to work also on other systems. -- `openwisp-network-topology `_: - provides way to collect and visualize network topology data from - dynamic mesh routing daemons or other network software (eg: OpenVPN); - it can be used in conjunction with openwisp-monitoring to get a better idea - of the state of the network -- `openwisp-firmware-upgrader `_: - automated firmware upgrades (single device or mass network upgrades) -- `openwisp-radius `_: - based on FreeRADIUS, allows to implement network access authentication systems like - 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 (802.11u) -- `openwisp-ipam `_: - it allows to manage the IP address space of networks +- `openwisp-controller + `_: network and WiFi + controller: provisioning, configuration management, x509 PKI management + and more; works on OpenWRT, but designed to work also on other systems. +- `openwisp-network-topology + `_: provides way + to collect and visualize network topology data from dynamic mesh routing + daemons or other network software (eg: OpenVPN); it can be used in + conjunction with openwisp-monitoring to get a better idea of the state + of the network +- `openwisp-firmware-upgrader + `_: automated + firmware upgrades (single device or mass network upgrades) +- `openwisp-radius `_: based + on FreeRADIUS, allows to implement network access authentication systems + like 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 + (802.11u) +- `openwisp-ipam `_: it allows + to manage the IP address space of networks **For a more complete overview of the OpenWISP modules and architecture**, -see the -`OpenWISP Architecture Overview +see the `OpenWISP Architecture Overview `_. .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png - :align: center + :align: center Available Features ------------------ -* Collection of monitoring information in a timeseries database (currently only influxdb is supported) -* Allows to browse alerts easily from the user interface with one click -* Collects and displays `device status <#device-status>`_ information like - uptime, RAM status, CPU load averages, - Interface properties and addresses, WiFi interface status and associated clients, - Neighbors information, DHCP Leases, Disk/Flash status -* Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, - `round trip time (latency) <#ping>`_, - `associated wifi clients <#wifi-clients>`_, `interface traffic <#traffic>`_, - `RAM usage <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage <#disk-usage>`_, - mobile signal (LTE/UMTS/GSM `signal strength <#mobile-signal-strength>`_, - `signal quality <#mobile-signal-quality>`_, - `access technology in use <#mobile-access-technology-in-use>`_), `bandwidth <#iperf3>`_, - `transferred data <#iperf3>`_, `restransmits <#iperf3>`_, `jitter <#iperf3>`_, - `datagram <#iperf3>`_, `datagram loss <#iperf3>`_ -* 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 the last 1 day, 3 days, 7 days, 30 days, and 365 days -* Configurable alerts -* CSV Export of monitoring data -* An overview of the status of the network is shown in the admin dashboard, - a chart shows the percentages of devices which are online, offline or having issues; - there are also `two timeseries charts which show the total unique WiFI clients and - the traffic flowing to the network `_, - a geographic map is also available for those who use the geographic features of OpenWISP -* Possibility to configure additional `Metrics <#openwisp_monitoring_metrics>`_ and `Charts <#openwisp_monitoring_charts>`_ -* Extensible active check system: it's possible to write additional checks that - are run periodically using python classes -* Extensible metrics and charts: it's possible to define new metrics and new charts -* API to retrieve the chart metrics and status information of each device - based on `NetJSON DeviceMonitoring `_ -* `Iperf3 check <#iperf3-1>`_ that provides network performance measurements such as maximum - achievable bandwidth, jitter, datagram loss etc of the openwrt device using `iperf3 utility `_ - ------------- +- Collection of monitoring information in a timeseries database (currently + only influxdb is supported) +- Allows to browse alerts easily from the user interface with one click +- Collects and displays `device status <#device-status>`_ information like + uptime, RAM status, CPU load averages, Interface properties and + addresses, WiFi interface status and associated clients, Neighbors + information, DHCP Leases, Disk/Flash status +- Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, `round + trip time (latency) <#ping>`_, `associated wifi clients + <#wifi-clients>`_, `interface traffic <#traffic>`_, `RAM usage + <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage + <#disk-usage>`_, mobile signal (LTE/UMTS/GSM `signal strength + <#mobile-signal-strength>`_, `signal quality <#mobile-signal-quality>`_, + `access technology in use <#mobile-access-technology-in-use>`_), + `bandwidth <#iperf3>`_, `transferred data <#iperf3>`_, `restransmits + <#iperf3>`_, `jitter <#iperf3>`_, `datagram <#iperf3>`_, `datagram loss + <#iperf3>`_ +- 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 the last 1 day, 3 days, 7 days, + 30 days, and 365 days +- Configurable alerts +- CSV Export of monitoring data +- An overview of the status of the network is shown in the admin + dashboard, a chart shows the percentages of devices which are online, + offline or having issues; there are also `two timeseries charts which + show the total unique WiFI clients and the traffic flowing to the + network `_, a geographic map is also + available for those who use the geographic features of OpenWISP +- Possibility to configure additional `Metrics + <#openwisp_monitoring_metrics>`_ and `Charts + <#openwisp_monitoring_charts>`_ +- Extensible active check system: it's possible to write additional checks + that are run periodically using python classes +- Extensible metrics and charts: it's possible to define new metrics and + new charts +- API to retrieve the chart metrics and status information of each device + based on `NetJSON DeviceMonitoring + `_ +- `Iperf3 check <#iperf3-1>`_ that provides network performance + measurements such as maximum achievable bandwidth, jitter, datagram loss + etc of the openwrt device using `iperf3 utility `_ + +---- .. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 + :backlinks: none + :depth: 3 ------------- +---- Contributing ------------ -Please refer to the `OpenWISP contributing guidelines `_. +Please refer to the `OpenWISP contributing guidelines +`_. diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst index d645d2ac..de514fe9 100644 --- a/docs/developer/developer-docs.rst +++ b/docs/developer/developer-docs.rst @@ -1,17 +1,17 @@ Developers Documentation ------------------------- +======================== .. include:: /partials/developers-docs-warning.rst .. toctree:: - :maxdepth: 1 + :maxdepth: 1 - ./installation.rst - ./management-commands.rst - ./monitoring-scripts.rst - ./registering-unregistering-metric-configuration.rst - ./registering-unregistering-chart-configuration.rst - ./registering-new-notification-types.rst - ./exceptions.rst - ./signals.rst - ./extending.rst + ./installation.rst + ./management-commands.rst + ./monitoring-scripts.rst + ./registering-unregistering-metric-configuration.rst + ./registering-unregistering-chart-configuration.rst + ./registering-new-notification-types.rst + ./exceptions.rst + ./signals.rst + ./extending.rst diff --git a/docs/developer/exceptions.rst b/docs/developer/exceptions.rst index 271a0f78..a8056471 100644 --- a/docs/developer/exceptions.rst +++ b/docs/developer/exceptions.rst @@ -1,29 +1,34 @@ Exceptions ----------- +========== .. include:: /partials/developers-docs-warning.rst ``TimeseriesWriteException`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- **Path**: ``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` -If there is any failure due while writing data in timeseries database, this exception shall -be raised with a helpful error message explaining the cause of the failure. -This exception will normally be caught and the failed write task will be retried in the background -so that there is no loss of data if failures occur due to overload of Timeseries server. -You can read more about this retry mechanism at `OPENWISP_MONITORING_WRITE_RETRY_OPTIONS <#openwisp-monitoring-write-retry-options>`_. +If there is any failure due while writing data in timeseries database, +this exception shall be raised with a helpful error message explaining the +cause of the failure. This exception will normally be caught and the +failed write task will be retried in the background so that there is no +loss of data if failures occur due to overload of Timeseries server. You +can read more about this retry mechanism at +`OPENWISP_MONITORING_WRITE_RETRY_OPTIONS +<#openwisp-monitoring-write-retry-options>`_. ``InvalidMetricConfigException`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------- -**Path**: ``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` +**Path**: +``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` This exception shall be raised if the metric configuration is broken. ``InvalidChartConfigException`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- -**Path**: ``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` +**Path**: +``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` This exception shall be raised if the chart configuration is broken. diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index efa5bc16..c768447b 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,47 +1,58 @@ Extending openwisp-monitoring ------------------------------ +============================= .. include:: /partials/developers-docs-warning.rst -One of the core values of the OpenWISP project is `Software Reusability `_, -for this reason *openwisp-monitoring* provides a set of base classes -which can be imported, extended and reused to create derivative apps. - -In order to implement your custom version of *openwisp-monitoring*, -you need to perform the steps described in the rest of this section. - -When in doubt, the code in the `test project `_ -and the ``sample apps`` namely `sample_check `_, -`sample_monitoring `_, `sample_device_monitoring `_ -will guide you in the correct direction: -just replicate and adapt that code to get a basic derivative of -*openwisp-monitoring* working. - -**Premise**: if you plan on using a customized version of this module, -we suggest to start with it since the beginning, because migrating your data +One of the core values of the OpenWISP project is `Software Reusability +`_, +for this reason *openwisp-monitoring* provides a set of base classes which +can be imported, extended and reused to create derivative apps. + +In order to implement your custom version of *openwisp-monitoring*, you +need to perform the steps described in the rest of this section. + +When in doubt, the code in the `test project +`_ +and the ``sample apps`` namely `sample_check +`_, +`sample_monitoring +`_, +`sample_device_monitoring +`_ +will guide you in the correct direction: just replicate and adapt that +code to get a basic derivative of *openwisp-monitoring* working. + +**Premise**: if you plan on using a customized version of this module, we +suggest to start with it since the beginning, because migrating your data from the default module to your extended version may be time consuming. 1. Initialize your custom module -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------- -The first thing you need to do in order to extend any *openwisp-monitoring* app is create -a new django app which will contain your custom version of that *openwisp-monitoring* app. +The first thing you need to do in order to extend any +*openwisp-monitoring* app is create a new django app which will contain +your custom version of that *openwisp-monitoring* app. -A django app is nothing more than a -`python package `_ -(a directory of python scripts), in the following examples we'll call these django apps as -``mycheck``, ``mydevicemonitoring``, ``mymonitoring`` but you can name it how you want:: +A django app is nothing more than a `python package +`_ (a directory +of python scripts), in the following examples we'll call these django apps +as ``mycheck``, ``mydevicemonitoring``, ``mymonitoring`` but you can name +it how you want: + +.. code-block:: django-admin startapp mycheck django-admin startapp mydevicemonitoring django-admin startapp mymonitoring -Keep in mind that the command mentioned above must be called from a directory -which is available in your `PYTHON_PATH `_ -so that you can then import the result into your project. +Keep in mind that the command mentioned above must be called from a +directory which is available in your `PYTHON_PATH +`_ so that +you can then import the result into your project. -Now you need to add ``mycheck`` to ``INSTALLED_APPS`` in your ``settings.py``, -ensuring also that ``openwisp_monitoring.check`` has been removed: +Now you need to add ``mycheck`` to ``INSTALLED_APPS`` in your +``settings.py``, ensuring also that ``openwisp_monitoring.check`` has been +removed: .. code-block:: python @@ -50,33 +61,38 @@ ensuring also that ``openwisp_monitoring.check`` has been removed: # 'openwisp_monitoring.check', <-- comment out or delete this line # 'openwisp_monitoring.device', <-- comment out or delete this line # 'openwisp_monitoring.monitoring' <-- comment out or delete this line - 'mycheck', - 'mydevicemonitoring', - 'mymonitoring', - 'nested_admin', + "mycheck", + "mydevicemonitoring", + "mymonitoring", + "nested_admin", ] -For more information about how to work with django projects and django apps, -please refer to the `"Tutorial: Writing your first Django app" in the django docunmentation `_. +For more information about how to work with django projects and django +apps, please refer to the `"Tutorial: Writing your first Django app" in +the django docunmentation +`_. 2. Install ``openwisp-monitoring`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------- + +Install (and add to the requirement of your project) +*openwisp-monitoring*: -Install (and add to the requirement of your project) *openwisp-monitoring*:: +.. code-block:: pip install --U https://github.com/openwisp/openwisp-monitoring/tarball/master 3. Add ``EXTENDED_APPS`` -~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ Add the following to your ``settings.py``: .. code-block:: python - EXTENDED_APPS = ['device_monitoring', 'monitoring', 'check'] + EXTENDED_APPS = ["device_monitoring", "monitoring", "check"] 4. Add ``openwisp_utils.staticfiles.DependencyFinder`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------------ Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` in your ``settings.py``: @@ -84,69 +100,83 @@ Add ``openwisp_utils.staticfiles.DependencyFinder`` to .. code-block:: python STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'openwisp_utils.staticfiles.DependencyFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "openwisp_utils.staticfiles.DependencyFinder", ] 5. Add ``openwisp_utils.loaders.DependencyLoader`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------- -Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: +Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your +``settings.py``: .. code-block:: python TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'OPTIONS': { - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - 'openwisp_utils.loaders.DependencyLoader', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "OPTIONS": { + "loaders": [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + "openwisp_utils.loaders.DependencyLoader", ], - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, } ] 6. Inherit the AppConfig class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------ Please refer to the following files in the sample app of the test project: -- `sample_check/__init__.py `_. -- `sample_check/apps.py `_. -- `sample_monitoring/__init__.py `_. -- `sample_monitoring/apps.py `_. -- `sample_device_monitoring/__init__.py `_. -- `sample_device_monitoring/apps.py `_. - -For more information regarding the concept of ``AppConfig`` please refer to -the `"Applications" section in the django documentation `_. +- `sample_check/__init__.py + `_. +- `sample_check/apps.py + `_. +- `sample_monitoring/__init__.py + `_. +- `sample_monitoring/apps.py + `_. +- `sample_device_monitoring/__init__.py + `_. +- `sample_device_monitoring/apps.py + `_. + +For more information regarding the concept of ``AppConfig`` please refer +to the `"Applications" section in the django documentation +`_. 7. Create your custom models -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- -To extend ``check`` app, refer to `sample_check models.py file `_. +To extend ``check`` app, refer to `sample_check models.py file +`_. -To extend ``monitoring`` app, refer to `sample_monitoring models.py file `_. +To extend ``monitoring`` app, refer to `sample_monitoring models.py file +`_. -To extend ``device_monitoring`` app, refer to `sample_device_monitoring models.py file `_. +To extend ``device_monitoring`` app, refer to `sample_device_monitoring +models.py file +`_. -**Note**: +.. note:: -- For doubts regarding how to use, extend or develop models please refer to - the `"Models" section in the django documentation `_. -- For doubts regarding proxy models please refer to `proxy models `_. + - For doubts regarding how to use, extend or develop models please + refer to the `"Models" section in the django documentation + `_. + - For doubts regarding proxy models please refer to `proxy models + `_. 8. Add swapper configurations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- Add the following to your ``settings.py``: @@ -154,49 +184,64 @@ Add the following to your ``settings.py``: # Setting models for swapper module # For extending check app - CHECK_CHECK_MODEL = 'YOUR_MODULE_NAME.Check' + CHECK_CHECK_MODEL = "YOUR_MODULE_NAME.Check" # For extending monitoring app - MONITORING_CHART_MODEL = 'YOUR_MODULE_NAME.Chart' - MONITORING_METRIC_MODEL = 'YOUR_MODULE_NAME.Metric' - MONITORING_ALERTSETTINGS_MODEL = 'YOUR_MODULE_NAME.AlertSettings' + MONITORING_CHART_MODEL = "YOUR_MODULE_NAME.Chart" + MONITORING_METRIC_MODEL = "YOUR_MODULE_NAME.Metric" + MONITORING_ALERTSETTINGS_MODEL = "YOUR_MODULE_NAME.AlertSettings" # 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' + 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``). +Substitute ```` with your actual django app name (also +known as ``app_label``). 9. Create database migrations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- -Create and apply database migrations:: +Create and apply database migrations: + +.. code-block:: ./manage.py makemigrations ./manage.py migrate -For more information, refer to the -`"Migrations" section in the django documentation `_. +For more information, refer to the `"Migrations" section in the django +documentation +`_. 10. Create your custom admin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- + +To extend ``check`` app, refer to `sample_check admin.py file +`_. -To extend ``check`` app, refer to `sample_check admin.py file `_. +To extend ``monitoring`` app, refer to `sample_monitoring admin.py file +`_. -To extend ``monitoring`` app, refer to `sample_monitoring admin.py file `_. +To extend ``device_monitoring`` app, refer to `sample_device_monitoring +admin.py file +`_. -To extend ``device_monitoring`` app, refer to `sample_device_monitoring admin.py file `_. +To introduce changes to the admin, you can do it in the two ways described +below. -To introduce changes to the admin, you can do it in the two ways described below. +.. note:: -**Note**: for doubts regarding how the django admin works, or how it can be customized, -please refer to `"The django admin site" section in the django documentation `_. + For doubts regarding how the django admin works, or how it can be + customized, please refer to `"The django admin site" section in the + django documentation + `_. 1. Monkey patching -################## +~~~~~~~~~~~~~~~~~~ -If the changes you need to add are relatively small, you can resort to monkey patching. +If the changes you need to add are relatively small, you can resort to +monkey patching. For example, for ``check`` app you can do it as: @@ -204,8 +249,8 @@ For example, for ``check`` app you can do it as: from openwisp_monitoring.check.admin import CheckAdmin - CheckAdmin.list_display.insert(1, 'my_custom_field') - CheckAdmin.ordering = ['-my_custom_field'] + CheckAdmin.list_display.insert(1, "my_custom_field") + CheckAdmin.ordering = ["-my_custom_field"] Similarly for ``device_monitoring`` app, you can do it as: @@ -213,26 +258,29 @@ Similarly for ``device_monitoring`` app, you can do it as: 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'] + 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: .. code-block:: python - from openwisp_monitoring.monitoring.admin import MetricAdmin, AlertSettingsAdmin + from openwisp_monitoring.monitoring.admin import ( + MetricAdmin, + AlertSettingsAdmin, + ) - MetricAdmin.list_display.insert(1, 'my_custom_field') - MetricAdmin.ordering = ['-my_custom_field'] - AlertSettingsAdmin.list_display.insert(1, 'my_custom_field') - AlertSettingsAdmin.ordering = ['-my_custom_field'] + MetricAdmin.list_display.insert(1, "my_custom_field") + MetricAdmin.ordering = ["-my_custom_field"] + AlertSettingsAdmin.list_display.insert(1, "my_custom_field") + AlertSettingsAdmin.ordering = ["-my_custom_field"] 2. Inheriting admin classes -########################### +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you need to introduce significant changes and/or you don't want to resort to -monkey patching, you can proceed as follows: +If you need to introduce significant changes and/or you don't want to +resort to monkey patching, you can proceed as follows: For ``check`` app, @@ -243,13 +291,15 @@ For ``check`` app, from openwisp_monitoring.check.admin import CheckAdmin as BaseCheckAdmin from swapper import load_model - Check = load_model('check', 'Check') + Check = load_model("check", "Check") admin.site.unregister(Check) + @admin.register(Check) class CheckAdmin(BaseCheckAdmin): # add your changes here + pass For ``device_monitoring`` app, @@ -257,23 +307,31 @@ 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 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') + 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 + pass + @admin.register(WifiSession) class WifiSessionAdmin(BaseWifiSessionAdmin): # add your changes here + pass For ``monitoring`` app, @@ -283,92 +341,113 @@ For ``monitoring`` app, from openwisp_monitoring.monitoring.admin import ( AlertSettingsAdmin as BaseAlertSettingsAdmin, - MetricAdmin as BaseMetricAdmin + MetricAdmin as BaseMetricAdmin, ) from swapper import load_model - Metric = load_model('Metric') - AlertSettings = load_model('AlertSettings') + Metric = load_model("Metric") + AlertSettings = load_model("AlertSettings") admin.site.unregister(Metric) admin.site.unregister(AlertSettings) + @admin.register(Metric) class MetricAdmin(BaseMetricAdmin): # add your changes here + pass + @admin.register(AlertSettings) class AlertSettingsAdmin(BaseAlertSettingsAdmin): # add your changes here + pass 11. Create root URL configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- -Please refer to the `urls.py `_ +Please refer to the `urls.py +`_ file in the test project. -For more information about URL configuration in django, please refer to the -`"URL dispatcher" section in the django documentation `_. +For more information about URL configuration in django, please refer to +the `"URL dispatcher" section in the django documentation +`_. 12. Create celery.py -~~~~~~~~~~~~~~~~~~~~ +-------------------- -Please refer to the `celery.py `_ +Please refer to the `celery.py +`_ file in the test project. -For more information about the usage of celery in django, please refer to the -`"First steps with Django" section in the celery documentation `_. +For more information about the usage of celery in django, please refer to +the `"First steps with Django" section in the celery documentation +`_. 13. Import Celery Tasks -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- -Add the following in your settings.py to import celery tasks from ``device_monitoring`` app. +Add the following in your settings.py to import celery tasks from +``device_monitoring`` app. .. code-block:: python - CELERY_IMPORTS = ('openwisp_monitoring.device.tasks',) + CELERY_IMPORTS = ("openwisp_monitoring.device.tasks",) 14. Create the custom command ``run_checks`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- -Please refer to the `run_checks.py `_ +Please refer to the `run_checks.py +`_ file in the test project. -For more information about the usage of custom management commands in django, please refer to the -`"Writing custom django-admin commands" section in the django documentation `_. +For more information about the usage of custom management commands in +django, please refer to the `"Writing custom django-admin commands" +section in the django documentation +`_. 15. Import the automated tests -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------ -When developing a custom application based on this module, it's a good idea -to import and run the base tests too, so that you can be sure the changes you're introducing -are not breaking some of the existing features of openwisp-monitoring. +When developing a custom application based on this module, it's a good +idea to import and run the base tests too, so that you can be sure the +changes you're introducing are not breaking some of the existing features +of openwisp-monitoring. -In case you need to add breaking changes, you can overwrite the tests defined -in the base classes to test your own behavior. +In case you need to add breaking changes, you can overwrite the tests +defined in the base classes to test your own behavior. -For, extending ``check`` app see the `tests of sample_check app `_ +For, extending ``check`` app see the `tests of sample_check app +`_ to find out how to do this. -For, extending ``device_monitoring`` app see the `tests of sample_device_monitoring app `_ +For, extending ``device_monitoring`` app see the `tests of +sample_device_monitoring app +`_ to find out how to do this. -For, extending ``monitoring`` app see the `tests of sample_monitoring app `_ +For, extending ``monitoring`` app see the `tests of sample_monitoring app +`_ to find out how to do this. Other base classes that can be inherited and extended -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------------- -**The following steps are not required and are intended for more advanced customization.** +**The following steps are not required and are intended for more advanced +customization.** ``DeviceMetricView`` -#################### +~~~~~~~~~~~~~~~~~~~~ -This view is responsible for displaying ``Charts`` and ``Status`` primarily. +This view is responsible for displaying ``Charts`` and ``Status`` +primarily. -The full python path is: ``openwisp_monitoring.device.api.views.DeviceMetricView``. +The full python path is: +``openwisp_monitoring.device.api.views.DeviceMetricView``. -If you want to extend this view, you will have to perform the additional steps below. +If you want to extend this view, you will have to perform the additional +steps below. Step 1. Import and extend view: @@ -376,9 +455,10 @@ Step 1. Import and extend view: # mydevice/api/views.py from openwisp_monitoring.device.api.views import ( - DeviceMetricView as BaseDeviceMetricView + DeviceMetricView as BaseDeviceMetricView, ) + class DeviceMetricView(BaseDeviceMetricView): # add your customizations here ... pass @@ -388,9 +468,9 @@ Step 2: remove the following line from your root ``urls.py`` file: .. code-block:: python re_path( - 'api/v1/monitoring/device/(?P[^/]+)/$', + "api/v1/monitoring/device/(?P[^/]+)/$", views.device_metric, - name='api_device_metric', + name="api_device_metric", ), Step 3: add an URL route pointing to your custom view in ``urls.py`` file: @@ -402,5 +482,9 @@ Step 3: add an URL route pointing to your custom view in ``urls.py`` file: urlpatterns = [ # ... other URLs - re_path(r'^(?P.*)$', DeviceMetricView.as_view(), name='api_device_metric',), + re_path( + r"^(?P.*)$", + DeviceMetricView.as_view(), + name="api_device_metric", + ), ] diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 8cd6dd73..bcb67046 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,10 +1,10 @@ Installation instructions -------------------------- +========================= .. include:: /partials/developers-docs-warning.rst Deploy it in production -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- See: @@ -14,12 +14,15 @@ See: .. _setup-integrate-in-an-existing-django-project: Install system dependencies -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- *openwisp-monitoring* uses InfluxDB to store metrics. Follow the -`installation instructions from InfluxDB's official documentation `_. +`installation instructions from InfluxDB's official documentation +`_. + +.. important:: -**Note:** Only *InfluxDB 1.8.x* is supported in *openwisp-monitoring*. + Only *InfluxDB 1.8.x* is supported in *openwisp-monitoring*. Install system packages: @@ -30,7 +33,7 @@ Install system packages: fping Install stable version from PyPI -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------- Install from PyPI: @@ -39,7 +42,7 @@ Install from PyPI: pip install openwisp-monitoring Install development version -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- Install tarball: @@ -53,15 +56,15 @@ Alternatively, you can install via pip using git: pip install -e git+git://github.com/openwisp/openwisp-monitoring#egg=openwisp_monitoring -If you want to contribute, follow the instructions in -`"Installing for development" <#installing-for-development>`_ section. +If you want to contribute, follow the instructions in `"Installing for +development" <#installing-for-development>`_ section. Installing for development -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- -Install the system dependencies as mentioned in the -`"Install system dependencies" <#install-system-dependencies>`_ section. -Install these additional packages that are required for development: +Install the system dependencies as mentioned in the `"Install system +dependencies" <#install-system-dependencies>`_ section. Install these +additional packages that are required for development: .. code-block:: shell @@ -87,14 +90,16 @@ Start Redis and InfluxDB using Docker: docker-compose up -d redis influxdb -Setup and activate a virtual-environment. (we'll be using `virtualenv `_) +Setup and activate a virtual-environment. (we'll be using `virtualenv +`_) .. code-block:: shell python -m virtualenv env source env/bin/activate -Make sure that you are using pip version 20.2.4 before moving to the next step: +Make sure that you are using pip version 20.2.4 before moving to the next +step: .. code-block:: shell @@ -108,8 +113,9 @@ Install development dependencies: pip install -r requirements-test.txt npm install -g jshint stylelint -Install WebDriver for Chromium for your browser version from ``_ -and extract ``chromedriver`` to one of directories from your ``$PATH`` (example: ``~/.local/bin/``). +Install WebDriver for Chromium for your browser version from +https://chromedriver.chromium.org/home and extract ``chromedriver`` to one +of directories from your ``$PATH`` (example: ``~/.local/bin/``). Create database: @@ -119,7 +125,8 @@ Create database: ./manage.py migrate ./manage.py createsuperuser -Run celery and celery-beat with the following commands (separate terminal windows are needed): +Run celery and celery-beat with the following commands (separate terminal +windows are needed): .. code-block:: shell @@ -148,11 +155,13 @@ Run quality assurance tests with: ./run-qa-checks Install and run on docker -~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------- + +.. note:: -**Note**: This Docker image is for development purposes only. -For the official OpenWISP Docker images, see: `docker-openwisp -`_. + This Docker image is for development purposes only. For the official + OpenWISP Docker images, see: `docker-openwisp + `_. Build from the Dockerfile: diff --git a/docs/developer/management-commands.rst b/docs/developer/management-commands.rst index 0a861844..4f173c59 100644 --- a/docs/developer/management-commands.rst +++ b/docs/developer/management-commands.rst @@ -1,16 +1,17 @@ Management commands -------------------- +=================== .. include:: /partials/developers-docs-warning.rst .. _run_checks: ``run_checks`` -~~~~~~~~~~~~~~ +-------------- -This command will execute all the `available checks `_ for all the devices. -By default checks are run periodically by *celery beat*. You can learn more -about this in :ref:`Setup `. +This command will execute all the `available checks `_ +for all the devices. By default checks are run periodically by *celery +beat*. You can learn more about this in :ref:`Setup +`. Example usage: @@ -20,7 +21,7 @@ Example usage: ./manage.py run_checks ``migrate_timeseries`` -~~~~~~~~~~~~~~~~~~~~~~ +---------------------- This command triggers asynchronous migration of the time-series database. diff --git a/docs/developer/monitoring-scripts.rst b/docs/developer/monitoring-scripts.rst index ab4f1cf7..c31bda55 100644 --- a/docs/developer/monitoring-scripts.rst +++ b/docs/developer/monitoring-scripts.rst @@ -1,42 +1,55 @@ Monitoring scripts ------------------- +================== .. include:: /partials/developers-docs-warning.rst -Monitoring scripts are now deprecated in favour of `monitoring packages `_. -Follow the migration guide in `Migrating from monitoring scripts to monitoring packages <#migrating-from-monitoring-scripts-to-monitoring-packages>`_ -section of this documentation. +Monitoring scripts are now deprecated in favour of `monitoring packages +`_. +Follow the migration guide in `Migrating from monitoring scripts to +monitoring packages +<#migrating-from-monitoring-scripts-to-monitoring-packages>`_ section of +this documentation. Migrating from monitoring scripts to monitoring packages --------------------------------------------------------- +======================================================== -This section is intended for existing users of *openwisp-monitoring*. -The older version of *openwisp-monitoring* used *monitoring scripts* that -are now deprecated in favour of `monitoring packages `_. +This section is intended for existing users of *openwisp-monitoring*. The +older version of *openwisp-monitoring* used *monitoring scripts* that are +now deprecated in favour of `monitoring packages +`_. If you already had a *monitoring template* created on your installation, -then the migrations of *openwisp-monitoring* will update that template -by making the following changes: +then the migrations of *openwisp-monitoring* will update that template by +making the following changes: - The file name of all scripts will be appended with ``legacy-`` keyword - in order to differentiate them from the scripts bundled with the new packages. -- The ``/usr/sbin/legacy-openwisp-monitoring`` (previously ``/usr/sbin/openwisp-monitoring``) - script will be updated to exit if `openwisp-monitoring package `_ + in order to differentiate them from the scripts bundled with the new + packages. +- The ``/usr/sbin/legacy-openwisp-monitoring`` (previously + ``/usr/sbin/openwisp-monitoring``) script will be updated to exit if + `openwisp-monitoring package + `_ is installed on the device. -Install the `monitoring packages `_ -as mentioned in the `Install monitoring packages on device <#install-monitoring-packages-on-the-device>`_ -section of this documentation. +Install the `monitoring packages +`_ +as mentioned in the `Install monitoring packages on device +<#install-monitoring-packages-on-the-device>`_ section of this +documentation. -After the proper configuration of the `openwisp-monitoring package `_ +After the proper configuration of the `openwisp-monitoring package +`_ on your device, you can remove the monitoring template from your devices. -We suggest removing the monitoring template from the devices one at a time instead -of deleting the template. This ensures the correctness of -*openwisp monitoring package* configuration and you'll not miss out on -any monitoring data. +We suggest removing the monitoring template from the devices one at a time +instead of deleting the template. This ensures the correctness of +*openwisp monitoring package* configuration and you'll not miss out on any +monitoring data. -**Note:** If you have made changes to the default monitoring template created -by *openwisp-monitoring* or you are using custom monitoring templates, then you should -remove such templates from the device before installing the -`monitoring packages `_. +.. note:: + + If you have made changes to the default monitoring template created by + *openwisp-monitoring* or you are using custom monitoring templates, + then you should remove such templates from the device before + installing the `monitoring packages + `_. diff --git a/docs/developer/registering-new-notification-types.rst b/docs/developer/registering-new-notification-types.rst index 06e7ed98..0c54b181 100644 --- a/docs/developer/registering-new-notification-types.rst +++ b/docs/developer/registering-new-notification-types.rst @@ -1,12 +1,15 @@ Registering new notification types ----------------------------------- +================================== .. include:: /partials/developers-docs-warning.rst -You can define your own notification types using ``register_notification_type`` function from OpenWISP -Notifications. For more information, see the relevant `openwisp-notifications section about registering notification types +You can define your own notification types using +``register_notification_type`` function from OpenWISP Notifications. For +more information, see the relevant `openwisp-notifications section about +registering notification types `_. -Once a new notification type is registered, you have to use the `"notify" signal provided in -openwisp-notifications `_ +Once a new notification type is registered, you have to use the `"notify" +signal provided in openwisp-notifications +`_ to send notifications for this type. diff --git a/docs/developer/registering-unregistering-chart-configuration.rst b/docs/developer/registering-unregistering-chart-configuration.rst index f02963ce..fd5085c1 100644 --- a/docs/developer/registering-unregistering-chart-configuration.rst +++ b/docs/developer/registering-unregistering-chart-configuration.rst @@ -1,24 +1,27 @@ Registering / Unregistering Chart Configuration ------------------------------------------------ +=============================================== .. include:: /partials/developers-docs-warning.rst -**OpenWISP Monitoring** provides registering and unregistering chart configuration through utility functions -``openwisp_monitoring.monitoring.configuration.register_chart`` and ``openwisp_monitoring.monitoring.configuration.unregister_chart``. -Using these functions you can register or unregister chart configurations from anywhere in your code. +**OpenWISP Monitoring** provides registering and unregistering chart +configuration through utility functions +``openwisp_monitoring.monitoring.configuration.register_chart`` and +``openwisp_monitoring.monitoring.configuration.unregister_chart``. Using +these functions you can register or unregister chart configurations from +anywhere in your code. ``register_chart`` -~~~~~~~~~~~~~~~~~~ +------------------ -This function is used to register a new chart configuration from anywhere in your code. +This function is used to register a new chart configuration from anywhere +in your code. -+--------------------------+-----------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------------+-----------------------------------------------------+ -| **chart_name**: | A ``str`` defining name of the chart configuration. | -+--------------------------+-----------------------------------------------------+ -| **chart_configuration**: | A ``dict`` defining configuration of the chart. | -+--------------------------+-----------------------------------------------------+ +======================== =============================================== +**Parameter** **Description** +**chart_name**: A ``str`` defining name of the chart + configuration. +**chart_configuration**: A ``dict`` defining configuration of the chart. +======================== =============================================== An example usage has been shown below. @@ -28,13 +31,13 @@ An example usage has been shown below. # Define configuration of your chart chart_config = { - 'type': 'histogram', - 'title': 'Histogram', - 'description': 'Histogram', - 'top_fields': 2, - 'order': 999, - 'query': { - 'influxdb': ( + "type": "histogram", + "title": "Histogram", + "description": "Histogram", + "top_fields": 2, + "order": 999, + "query": { + "influxdb": ( "SELECT {fields|SUM|/ 1} FROM {key} " "WHERE time >= '{time}' AND content_type = " "'{content_type}' AND object_id = '{object_id}'" @@ -43,24 +46,28 @@ An example usage has been shown below. } # Register your custom chart configuration - register_chart('chart_name', chart_config) + register_chart("chart_name", chart_config) -**Note**: It will raise ``ImproperlyConfigured`` exception if a chart configuration -is already registered with same name (not to be confused with verbose_name). +.. note:: -If you don't need to register a new chart but need to change a specific key of an -existing chart configuration, you can use :ref:`OPENWISP_MONITORING_CHARTS `. + It will raise ``ImproperlyConfigured`` exception if a chart + configuration is already registered with same name (not to be confused + with verbose_name). + +If you don't need to register a new chart but need to change a specific +key of an existing chart configuration, you can use +:ref:`OPENWISP_MONITORING_CHARTS `. ``unregister_chart`` -~~~~~~~~~~~~~~~~~~~~ +-------------------- -This function is used to unregister a chart configuration from anywhere in your code. +This function is used to unregister a chart configuration from anywhere in +your code. -+------------------+-----------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+-----------------------------------------------------+ -| **chart_name**: | A ``str`` defining name of the chart configuration. | -+------------------+-----------------------------------------------------+ +=============== =================================================== +**Parameter** **Description** +**chart_name**: A ``str`` defining name of the chart configuration. +=============== =================================================== An example usage is shown below. @@ -69,7 +76,9 @@ An example usage is shown below. from openwisp_monitoring.monitoring.configuration import unregister_chart # Unregister previously registered chart configuration - unregister_chart('chart_name') + unregister_chart("chart_name") + +.. note:: -**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned chart -configuration is not registered. + It will raise ``ImproperlyConfigured`` exception if the concerned + chart configuration is not registered. diff --git a/docs/developer/registering-unregistering-metric-configuration.rst b/docs/developer/registering-unregistering-metric-configuration.rst index b874756f..3a933bd3 100644 --- a/docs/developer/registering-unregistering-metric-configuration.rst +++ b/docs/developer/registering-unregistering-metric-configuration.rst @@ -1,24 +1,27 @@ Registering / Unregistering Metric Configuration ------------------------------------------------- +================================================ .. include:: /partials/developers-docs-warning.rst -**OpenWISP Monitoring** provides registering and unregistering metric configuration through utility functions -``openwisp_monitoring.monitoring.configuration.register_metric`` and ``openwisp_monitoring.monitoring.configuration.unregister_metric``. -Using these functions you can register or unregister metric configurations from anywhere in your code. +**OpenWISP Monitoring** provides registering and unregistering metric +configuration through utility functions +``openwisp_monitoring.monitoring.configuration.register_metric`` and +``openwisp_monitoring.monitoring.configuration.unregister_metric``. Using +these functions you can register or unregister metric configurations from +anywhere in your code. ``register_metric`` -~~~~~~~~~~~~~~~~~~~ +------------------- -This function is used to register a new metric configuration from anywhere in your code. +This function is used to register a new metric configuration from anywhere +in your code. -+--------------------------+------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------------+------------------------------------------------------+ -| **metric_name**: | A ``str`` defining name of the metric configuration. | -+--------------------------+------------------------------------------------------+ -|**metric_configuration**: | A ``dict`` defining configuration of the metric. | -+--------------------------+------------------------------------------------------+ +========================= ================================================ +**Parameter** **Description** +**metric_name**: A ``str`` defining name of the metric + configuration. +**metric_configuration**: A ``dict`` defining configuration of the metric. +========================= ================================================ An example usage has been shown below. @@ -29,134 +32,141 @@ An example usage has been shown below. # Define configuration of your metric metric_config = { - 'label': _('Ping'), - 'name': 'Ping', - 'key': 'ping', - 'field_name': 'reachable', - 'related_fields': ['loss', 'rtt_min', 'rtt_max', 'rtt_avg'], - 'charts': { - 'uptime': { - 'type': 'bar', - 'title': _('Uptime'), - 'description': _( - 'A value of 100% means reachable, 0% means unreachable, values in ' - 'between 0% and 100% indicate the average reachability in the ' - 'period observed. Obtained with the fping linux program.' + "label": _("Ping"), + "name": "Ping", + "key": "ping", + "field_name": "reachable", + "related_fields": ["loss", "rtt_min", "rtt_max", "rtt_avg"], + "charts": { + "uptime": { + "type": "bar", + "title": _("Uptime"), + "description": _( + "A value of 100% means reachable, 0% means unreachable, values in " + "between 0% and 100% indicate the average reachability in the " + "period observed. Obtained with the fping linux program." ), - 'summary_labels': [_('Average uptime')], - 'unit': '%', - 'order': 200, - 'colorscale': { - 'max': 100, - 'min': 0, - 'label': _('Reachable'), - 'scale': [ - [[0, '#c13000'], - [0.1,'cb7222'], - [0.5,'#deed0e'], - [0.9, '#7db201'], - [1, '#498b26']], + "summary_labels": [_("Average uptime")], + "unit": "%", + "order": 200, + "colorscale": { + "max": 100, + "min": 0, + "label": _("Reachable"), + "scale": [ + [ + [0, "#c13000"], + [0.1, "cb7222"], + [0.5, "#deed0e"], + [0.9, "#7db201"], + [1, "#498b26"], + ], ], - 'map': [ - [100, '#498b26', _('Reachable')], - [90, '#7db201', _('Mostly Reachable')], - [50, '#deed0e', _('Partly Reachable')], - [10, '#cb7222', _('Mostly Unreachable')], - [None, '#c13000', _('Unreachable')], + "map": [ + [100, "#498b26", _("Reachable")], + [90, "#7db201", _("Mostly Reachable")], + [50, "#deed0e", _("Partly Reachable")], + [10, "#cb7222", _("Mostly Unreachable")], + [None, "#c13000", _("Unreachable")], ], - 'fixed_value': 100, + "fixed_value": 100, }, - 'query': chart_query['uptime'], + "query": chart_query["uptime"], }, - 'packet_loss': { - 'type': 'bar', - 'title': _('Packet loss'), - 'description': _( - 'Indicates the percentage of lost packets observed in ICMP probes. ' - 'Obtained with the fping linux program.' + "packet_loss": { + "type": "bar", + "title": _("Packet loss"), + "description": _( + "Indicates the percentage of lost packets observed in ICMP probes. " + "Obtained with the fping linux program." ), - 'summary_labels': [_('Average packet loss')], - 'unit': '%', - 'colors': '#d62728', - 'order': 210, - 'query': chart_query['packet_loss'], + "summary_labels": [_("Average packet loss")], + "unit": "%", + "colors": "#d62728", + "order": 210, + "query": chart_query["packet_loss"], }, - 'rtt': { - 'type': 'scatter', - 'title': _('Round Trip Time'), - 'description': _( - 'Round trip time observed in ICMP probes, measuered in milliseconds.' + "rtt": { + "type": "scatter", + "title": _("Round Trip Time"), + "description": _( + "Round trip time observed in ICMP probes, measuered in milliseconds." ), - 'summary_labels': [ - _('Average RTT'), - _('Average Max RTT'), - _('Average Min RTT'), + "summary_labels": [ + _("Average RTT"), + _("Average Max RTT"), + _("Average Min RTT"), ], - 'unit': _(' ms'), - 'order': 220, - 'query': chart_query['rtt'], + "unit": _(" ms"), + "order": 220, + "query": chart_query["rtt"], }, }, - 'alert_settings': {'operator': '<', 'threshold': 1, 'tolerance': 0}, - 'notification': { - 'problem': { - 'verbose_name': 'Ping PROBLEM', - 'verb': 'cannot be reached anymore', - 'level': 'warning', - 'email_subject': _( - '[{site.name}] {notification.target} is not reachable' + "alert_settings": {"operator": "<", "threshold": 1, "tolerance": 0}, + "notification": { + "problem": { + "verbose_name": "Ping PROBLEM", + "verb": "cannot be reached anymore", + "level": "warning", + "email_subject": _( + "[{site.name}] {notification.target} is not reachable" ), - 'message': _( - 'The device [{notification.target}] {notification.verb} anymore by our ping ' - 'messages.' + "message": _( + "The device [{notification.target}] {notification.verb} anymore by our ping " + "messages." ), }, - 'recovery': { - 'verbose_name': 'Ping RECOVERY', - 'verb': 'has become reachable', - 'level': 'info', - 'email_subject': _( - '[{site.name}] {notification.target} is reachable again' + "recovery": { + "verbose_name": "Ping RECOVERY", + "verb": "has become reachable", + "level": "info", + "email_subject": _( + "[{site.name}] {notification.target} is reachable again" ), - 'message': _( - 'The device [{notification.target}] {notification.verb} again by our ping ' - 'messages.' + "message": _( + "The device [{notification.target}] {notification.verb} again by our ping " + "messages." ), }, }, } # Register your custom metric configuration - register_metric('ping', metric_config) + register_metric("ping", metric_config) -The above example will register one metric configuration (named ``ping``), three chart -configurations (named ``rtt``, ``packet_loss``, ``uptime``) as defined in the **charts** key, -two notification types (named ``ping_recovery``, ``ping_problem``) as defined in **notification** key. +The above example will register one metric configuration (named ``ping``), +three chart configurations (named ``rtt``, ``packet_loss``, ``uptime``) as +defined in the **charts** key, two notification types (named +``ping_recovery``, ``ping_problem``) as defined in **notification** key. -The ``AlertSettings`` of ``ping`` metric will by default use ``threshold`` and ``tolerance`` -defined in the ``alert_settings`` key. -You can always override them and define your own custom values via the *admin*. +The ``AlertSettings`` of ``ping`` metric will by default use ``threshold`` +and ``tolerance`` defined in the ``alert_settings`` key. You can always +override them and define your own custom values via the *admin*. -You can also use the ``alert_field`` key in metric configuration -which allows ``AlertSettings`` to check the ``threshold`` on -``alert_field`` instead of the default ``field_name`` key. +You can also use the ``alert_field`` key in metric configuration which +allows ``AlertSettings`` to check the ``threshold`` on ``alert_field`` +instead of the default ``field_name`` key. -**Note**: It will raise ``ImproperlyConfigured`` exception if a metric configuration -is already registered with same name (not to be confused with verbose_name). +.. note:: -If you don't need to register a new metric but need to change a specific key of an -existing metric configuration, you can use :ref:`OPENWISP_MONITORING_METRICS `. + It will raise ``ImproperlyConfigured`` exception if a metric + configuration is already registered with same name (not to be confused + with verbose_name). + +If you don't need to register a new metric but need to change a specific +key of an existing metric configuration, you can use +:ref:`OPENWISP_MONITORING_METRICS `. ``unregister_metric`` -~~~~~~~~~~~~~~~~~~~~~ +--------------------- -This function is used to unregister a metric configuration from anywhere in your code. +This function is used to unregister a metric configuration from anywhere +in your code. -+------------------+------------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+------------------------------------------------------+ -| **metric_name**: | A ``str`` defining name of the metric configuration. | -+------------------+------------------------------------------------------+ +================ ==================================================== +**Parameter** **Description** +**metric_name**: A ``str`` defining name of the metric configuration. +================ ==================================================== An example usage is shown below. @@ -165,7 +175,9 @@ An example usage is shown below. from openwisp_monitoring.monitoring.configuration import unregister_metric # Unregister previously registered metric configuration - unregister_metric('metric_name') + unregister_metric("metric_name") + +.. note:: -**Note**: It will raise ``ImproperlyConfigured`` exception if the concerned metric -configuration is not registered. + It will raise ``ImproperlyConfigured`` exception if the concerned + metric configuration is not registered. diff --git a/docs/developer/signals.rst b/docs/developer/signals.rst index 673b19d2..bb742445 100644 --- a/docs/developer/signals.rst +++ b/docs/developer/signals.rst @@ -1,10 +1,10 @@ Signals -------- +======= .. include:: /partials/developers-docs-warning.rst ``device_metrics_received`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- **Path**: ``openwisp_monitoring.device.signals.device_metrics_received`` @@ -12,40 +12,48 @@ Signals - ``instance``: instance of ``Device`` whose metrics have been received - ``request``: the HTTP request object -- ``time``: time with which metrics will be saved. If none, then server time will be used -- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues +- ``time``: time with which metrics will be saved. If none, then server + time will be used +- ``current``: whether the data has just been collected or was collected + previously and sent now due to network connectivity issues -This signal is emitted when device metrics are received to the ``DeviceMetric`` -view (only when using HTTP POST). +This signal is emitted when device metrics are received to the +``DeviceMetric`` view (only when using HTTP POST). -The signal is emitted just before a successful response is returned, -it is not sent if the response was not successful. +The signal is emitted just before a successful response is returned, it is +not sent if the response was not successful. ``health_status_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------- **Path**: ``openwisp_monitoring.device.signals.health_status_changed`` **Arguments**: -- ``instance``: instance of ``DeviceMonitoring`` whose status has been changed -- ``status``: the status by which DeviceMonitoring's existing status has been updated with +- ``instance``: instance of ``DeviceMonitoring`` whose status has been + changed +- ``status``: the status by which DeviceMonitoring's existing status has + been updated with -This signal is emitted only if the health status of DeviceMonitoring object gets updated. +This signal is emitted only if the health status of DeviceMonitoring +object gets updated. ``threshold_crossed`` -~~~~~~~~~~~~~~~~~~~~~ +--------------------- **Path**: ``openwisp_monitoring.monitoring.signals.threshold_crossed`` **Arguments**: -- ``metric``: ``Metric`` object whose threshold defined in related alert settings was crossed +- ``metric``: ``Metric`` object whose threshold defined in related alert + settings was crossed - ``alert_settings``: ``AlertSettings`` related to the ``Metric`` - ``target``: related ``Device`` object -- ``first_time``: it will be set to true when the metric is written for the first time. It shall be set to false afterwards. -- ``tolerance_crossed``: it will be set to true if the metric has crossed the threshold for tolerance configured in alert settings. - Otherwise, it will be set to false. +- ``first_time``: it will be set to true when the metric is written for + the first time. It shall be set to false afterwards. +- ``tolerance_crossed``: it will be set to true if the metric has crossed + the threshold for tolerance configured in alert settings. Otherwise, it + will be set to false. ``first_time`` parameter can be used to avoid initiating unneeded actions. For example, sending recovery notifications. @@ -54,31 +62,35 @@ This signal is emitted when the threshold value of a ``Metric`` defined in alert settings is crossed. ``pre_metric_write`` -~~~~~~~~~~~~~~~~~~~~ +-------------------- **Path**: ``openwisp_monitoring.monitoring.signals.pre_metric_write`` **Arguments**: -- ``metric``: ``Metric`` object whose data shall be stored in timeseries database +- ``metric``: ``Metric`` object whose data shall be stored in timeseries + database - ``values``: metric data that shall be stored in the timeseries database - ``time``: time with which metrics will be saved -- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues +- ``current``: whether the data has just been collected or was collected + previously and sent now due to network connectivity issues -This signal is emitted for every metric before the write operation is sent to -the timeseries database. +This signal is emitted for every metric before the write operation is sent +to the timeseries database. ``post_metric_write`` -~~~~~~~~~~~~~~~~~~~~~ +--------------------- **Path**: ``openwisp_monitoring.monitoring.signals.post_metric_write`` **Arguments**: -- ``metric``: ``Metric`` object whose data is being stored in timeseries database +- ``metric``: ``Metric`` object whose data is being stored in timeseries + database - ``values``: metric data that is being stored in the timeseries database - ``time``: time with which metrics will be saved -- ``current``: whether the data has just been collected or was collected previously and sent now due to network connectivity issues +- ``current``: whether the data has just been collected or was collected + previously and sent now due to network connectivity issues -This signal is emitted for every metric after the write operation is successfully -executed in the background. +This signal is emitted for every metric after the write operation is +successfully executed in the background. diff --git a/docs/index.rst b/docs/index.rst index a46a9903..539d3f46 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,94 +1,106 @@ OpenWISP Monitoring =================== -OpenWISP Monitoring is a network monitoring system written in Python and Django, -designed to be **extensible**, **programmable**, **scalable** and easy to use by end users: -once the system is configured, monitoring checks, alerts and metric collection -happens automatically. +OpenWISP Monitoring is a network monitoring system written in Python and +Django, designed to be **extensible**, **programmable**, **scalable** and +easy to use by end users: once the system is configured, monitoring +checks, alerts and metric collection happens automatically. See the `available features <#available-features>`_. -`OpenWISP `_ is not only an application designed for end users, -but can also be used as a framework on which custom network automation solutions can be -built on top of its building blocks. +`OpenWISP `_ is not only an application designed for +end users, but can also be used as a framework on which custom network +automation solutions can be built on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: -- `openwisp-controller `_: - network and WiFi controller: provisioning, configuration management, - x509 PKI management and more; works on OpenWRT, but designed to work also on other systems. -- `openwisp-network-topology `_: - provides way to collect and visualize network topology data from - dynamic mesh routing daemons or other network software (eg: OpenVPN); - it can be used in conjunction with openwisp-monitoring to get a better idea - of the state of the network -- `openwisp-firmware-upgrader `_: - automated firmware upgrades (single device or mass network upgrades) -- `openwisp-radius `_: - based on FreeRADIUS, allows to implement network access authentication systems like - 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 (802.11u) -- `openwisp-ipam `_: - it allows to manage the IP address space of networks +- `openwisp-controller + `_: network and WiFi + controller: provisioning, configuration management, x509 PKI management + and more; works on OpenWRT, but designed to work also on other systems. +- `openwisp-network-topology + `_: provides way + to collect and visualize network topology data from dynamic mesh routing + daemons or other network software (eg: OpenVPN); it can be used in + conjunction with openwisp-monitoring to get a better idea of the state + of the network +- `openwisp-firmware-upgrader + `_: automated + firmware upgrades (single device or mass network upgrades) +- `openwisp-radius `_: based + on FreeRADIUS, allows to implement network access authentication systems + like 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 + (802.11u) +- `openwisp-ipam `_: it allows + to manage the IP address space of networks **For a more complete overview of the OpenWISP modules and architecture**, -see the -`OpenWISP Architecture Overview +see the `OpenWISP Architecture Overview `_. .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png - :align: center + :align: center **Available Features** -* Collection of monitoring information in a timeseries database (currently only influxdb is supported) -* Allows to browse alerts easily from the user interface with one click -* Collects and displays `device status <#device-status>`_ information like - uptime, RAM status, CPU load averages, - Interface properties and addresses, WiFi interface status and associated clients, - Neighbors information, DHCP Leases, Disk/Flash status -* Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, - `round trip time (latency) <#ping>`_, - `associated wifi clients <#wifi-clients>`_, `interface traffic <#traffic>`_, - `RAM usage <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage <#disk-usage>`_, - mobile signal (LTE/UMTS/GSM `signal strength <#mobile-signal-strength>`_, - `signal quality <#mobile-signal-quality>`_, - `access technology in use <#mobile-access-technology-in-use>`_), `bandwidth <#iperf3>`_, - `transferred data <#iperf3>`_, `restransmits <#iperf3>`_, `jitter <#iperf3>`_, - `datagram <#iperf3>`_, `datagram loss <#iperf3>`_ -* 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 the last 1 day, 3 days, 7 days, 30 days, and 365 days -* Configurable alerts -* CSV Export of monitoring data -* An overview of the status of the network is shown in the admin dashboard, - a chart shows the percentages of devices which are online, offline or having issues; - there are also `two timeseries charts which show the total unique WiFI clients and - the traffic flowing to the network `_, - a geographic map is also available for those who use the geographic features of OpenWISP -* Possibility to configure additional :ref:`Metrics ` and :ref:`Charts ` -* Extensible active check system: it's possible to write additional checks that - are run periodically using python classes -* Extensible metrics and charts: it's possible to define new metrics and new charts -* API to retrieve the chart metrics and status information of each device - based on `NetJSON DeviceMonitoring `_ -* :ref:`Iperf3 check ` that provides network performance measurements such as maximum - achievable bandwidth, jitter, datagram loss etc of the openwrt device using `iperf3 utility `_ +- Collection of monitoring information in a timeseries database (currently + only influxdb is supported) +- Allows to browse alerts easily from the user interface with one click +- Collects and displays `device status <#device-status>`_ information like + uptime, RAM status, CPU load averages, Interface properties and + addresses, WiFi interface status and associated clients, Neighbors + information, DHCP Leases, Disk/Flash status +- Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, `round + trip time (latency) <#ping>`_, `associated wifi clients + <#wifi-clients>`_, `interface traffic <#traffic>`_, `RAM usage + <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage + <#disk-usage>`_, mobile signal (LTE/UMTS/GSM `signal strength + <#mobile-signal-strength>`_, `signal quality <#mobile-signal-quality>`_, + `access technology in use <#mobile-access-technology-in-use>`_), + `bandwidth <#iperf3>`_, `transferred data <#iperf3>`_, `restransmits + <#iperf3>`_, `jitter <#iperf3>`_, `datagram <#iperf3>`_, `datagram loss + <#iperf3>`_ +- 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 the last 1 day, 3 days, 7 days, + 30 days, and 365 days +- Configurable alerts +- CSV Export of monitoring data +- An overview of the status of the network is shown in the admin + dashboard, a chart shows the percentages of devices which are online, + offline or having issues; there are also `two timeseries charts which + show the total unique WiFI clients and the traffic flowing to the + network `_, a geographic map is also + available for those who use the geographic features of OpenWISP +- Possibility to configure additional :ref:`Metrics + ` and :ref:`Charts + ` +- Extensible active check system: it's possible to write additional checks + that are run periodically using python classes +- Extensible metrics and charts: it's possible to define new metrics and + new charts +- API to retrieve the chart metrics and status information of each device + based on `NetJSON DeviceMonitoring + `_ +- :ref:`Iperf3 check ` that provides network performance + measurements such as maximum achievable bandwidth, jitter, datagram loss + etc of the openwrt device using `iperf3 utility `_ .. toctree:: - :maxdepth: 1 + :maxdepth: 1 - ./user/quickstart.rst - ./user/passive-vs-active-metric-collection.rst - ./user/device-health-status.rst - ./user/default-metrics.rst - ./user/dashboard-monitoring-charts.rst - ./user/adaptive-size-charts.rst - ./user/wifi-sessions.rst - ./user/default-alerts-and-notifications.rst - ./user/available-checks.rst - ./user/iperf3-usage-instructions.rst - ./user/adding-checks-and-alertsettings.rst - ./user/settings.rst - ./user/rest-api.rst - ./developer/developer-docs.rst + ./user/quickstart.rst + ./user/passive-vs-active-metric-collection.rst + ./user/device-health-status.rst + ./user/default-metrics.rst + ./user/dashboard-monitoring-charts.rst + ./user/adaptive-size-charts.rst + ./user/wifi-sessions.rst + ./user/default-alerts-and-notifications.rst + ./user/available-checks.rst + ./user/iperf3-usage-instructions.rst + ./user/adding-checks-and-alertsettings.rst + ./user/settings.rst + ./user/rest-api.rst + ./developer/developer-docs.rst diff --git a/docs/user/adaptive-size-charts.rst b/docs/user/adaptive-size-charts.rst index 4c8a3c45..caffba4a 100644 --- a/docs/user/adaptive-size-charts.rst +++ b/docs/user/adaptive-size-charts.rst @@ -1,29 +1,30 @@ Adaptive size charts --------------------- +==================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/adaptive-chart.png - :align: center + :align: center -When configuring charts, it is possible to flag their unit -as ``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `K`, `M`, `G` and `T` depending on -the size of each point, the summary values and Y axis are also resized. +When configuring charts, it is possible to flag their unit as +``adaptive_prefix``, this allows to make the charts more readable because +the units are shown in either `K`, `M`, `G` and `T` depending on the size +of each point, the summary values and Y axis are also resized. Example taken from the default configuration of the traffic chart: .. code-block:: python - 'traffic': { - # other configurations for this chart - - # traffic measured in 'B' (bytes) - # unit B, KB, MB, GB, TB - 'unit': 'adaptive_prefix+B', - }, - - 'bandwidth': { - # adaptive unit for bandwidth related charts - # bandwidth measured in 'bps'(bits/sec) - # unit bps, Kbps, Mbps, Gbps, Tbps - 'unit': 'adaptive_prefix+bps', - }, + OPENWISP_MONITORING_CHARTS = { + "traffic": { + # other configurations for this chart + # traffic measured in 'B' (bytes) + # unit B, KB, MB, GB, TB + "unit": "adaptive_prefix+B", + }, + "bandwidth": { + # other configurations for this chart + # adaptive unit for bandwidth related charts + # bandwidth measured in 'bps'(bits/sec) + # unit bps, Kbps, Mbps, Gbps, Tbps + "unit": "adaptive_prefix+bps", + }, + } diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst index af030435..aa629715 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -1,70 +1,81 @@ +.. _adding_checks_and_alertsettings: + Adding Checks and Alert settings from the device page ------------------------------------------------------ +===================================================== -We can add checks and define alert settings directly from the **device page**. +We can add checks and define alert settings directly from the **device +page**. -To add a check, you just need to select an available **check type** as shown below: +To add a check, you just need to select an available **check type** as +shown below: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-check.png - :align: center + :align: center The following example shows how to use the :ref:`OPENWISP_MONITORING_METRICS setting ` -to reconfigure the system for :ref:`iperf3 check ` to send an alert if -the measured **TCP bandwidth** has been less than **10 Mbit/s** for more than **2 days**. +to reconfigure the system for :ref:`iperf3 check ` to send an +alert if the measured **TCP bandwidth** has been less than **10 Mbit/s** +for more than **2 days**. -1. By default, :ref:`Iperf3 checks ` come with default alert settings, -but it is easy to customize alert settings through the device page as shown below: +1. By default, :ref:`Iperf3 checks ` come with default alert +settings, but it is easy to customize alert settings through the device +page as shown below: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-alertsettings.png - :align: center + :align: center -2. Now, add the following notification configuration to send an alert for **TCP bandwidth**: +2. Now, add the following notification configuration to send an alert for + **TCP bandwidth**: .. code-block:: python - # Main project settings.py - from django.utils.translation import gettext_lazy as _ - - OPENWISP_MONITORING_METRICS = { - 'iperf3': { - 'notification': { - 'problem': { - 'verbose_name': 'Iperf3 PROBLEM', - 'verb': _('Iperf3 bandwidth is less than normal value'), - 'level': 'warning', - 'email_subject': _( - '[{site.name}] PROBLEM: {notification.target} {notification.verb}' - ), - 'message': _( - 'The device [{notification.target}]({notification.target_link}) ' - '{notification.verb}.' - ), - }, - 'recovery': { - 'verbose_name': 'Iperf3 RECOVERY', - 'verb': _('Iperf3 bandwidth now back to normal'), - 'level': 'info', - 'email_subject': _( - '[{site.name}] RECOVERY: {notification.target} {notification.verb}' - ), - 'message': _( - 'The device [{notification.target}]({notification.target_link}) ' - '{notification.verb}.' - ), - }, - }, - }, - } + # Main project settings.py + from django.utils.translation import gettext_lazy as _ + + OPENWISP_MONITORING_METRICS = { + "iperf3": { + "notification": { + "problem": { + "verbose_name": "Iperf3 PROBLEM", + "verb": _("Iperf3 bandwidth is less than normal value"), + "level": "warning", + "email_subject": _( + "[{site.name}] PROBLEM: {notification.target} {notification.verb}" + ), + "message": _( + "The device [{notification.target}]({notification.target_link}) " + "{notification.verb}." + ), + }, + "recovery": { + "verbose_name": "Iperf3 RECOVERY", + "verb": _("Iperf3 bandwidth now back to normal"), + "level": "info", + "email_subject": _( + "[{site.name}] RECOVERY: {notification.target} {notification.verb}" + ), + "message": _( + "The device [{notification.target}]({notification.target_link}) " + "{notification.verb}." + ), + }, + }, + }, + } .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_warn.png - :align: center + :align: center .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_info.png - :align: center + :align: center + +.. note:: -**Note:** To access the features described above, the user must have permissions for ``Check`` and ``AlertSetting`` inlines, -these permissions are included by default in the "Administrator" and "Operator" groups and are shown in the screenshot below. + To access the features described above, the user must have permissions + for ``Check`` and ``AlertSetting`` inlines, these permissions are + included by default in the "Administrator" and "Operator" groups and + are shown in the screenshot below. .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/inline-permissions.png - :align: center + :align: center diff --git a/docs/user/available-checks.rst b/docs/user/available-checks.rst index 00e2d256..fd088ce8 100644 --- a/docs/user/available-checks.rst +++ b/docs/user/available-checks.rst @@ -1,25 +1,29 @@ Available Checks ----------------- +================ Ping -~~~~ +---- -This check returns information on device ``uptime`` and ``RTT (Round trip time)``. -The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. The ``fping`` -command is used to collect these metrics. -You may choose to disable auto creation of this check by setting -:ref:`OPENWISP_MONITORING_AUTO_PING ` to ``False``. +This check returns information on device ``uptime`` and ``RTT (Round trip +time)``. The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. +The ``fping`` command is used to collect these metrics. You may choose to +disable auto creation of this check by setting +:ref:`OPENWISP_MONITORING_AUTO_PING ` to +``False``. You can change the default values used for ping checks using -:ref:`OPENWISP_MONITORING_PING_CHECK_CONFIG ` setting. +:ref:`OPENWISP_MONITORING_PING_CHECK_CONFIG +` setting. Configuration applied -~~~~~~~~~~~~~~~~~~~~~ +--------------------- -This check ensures that the `openwisp-config agent `_ -is running and applying configuration changes in a timely manner. -You may choose to disable auto creation of this check by using the -setting :ref:`OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK `. +This check ensures that the `openwisp-config agent +`_ is running and applying +configuration changes in a timely manner. You may choose to disable auto +creation of this check by using the setting +:ref:`OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK +`. This check runs periodically, but it is also triggered whenever the configuration status of a device changes, this ensures the check reacts @@ -29,23 +33,27 @@ if there's anything that is not working as intended. .. _iperf3-1: Iperf3 -~~~~~~ +------ -This check provides network performance measurements such as maximum achievable bandwidth, -jitter, datagram loss etc of the device using `iperf3 utility `_. +This check provides network performance measurements such as maximum +achievable bandwidth, jitter, datagram loss etc of the device using +`iperf3 utility `_. -This check is **disabled by default**. You can enable auto creation of this check by setting the -:ref:`OPENWISP_MONITORING_AUTO_IPERF3 ` to ``True``. +This check is **disabled by default**. You can enable auto creation of +this check by setting the :ref:`OPENWISP_MONITORING_AUTO_IPERF3 +` to ``True``. -You can also :ref:`add the iperf3 check -`_ directly from the device page. +You can also :ref:`add the iperf3 check ` +directly from the device page. -It also supports tuning of various parameters. +It also supports tuning of various parameters. You can change the +parameters used for iperf3 checks (e.g. timing, port, username, password, +rsa_publc_key etc) using the +:ref:`openwisp_monitoring_iperf3_check_config` setting. -You can also change the parameters used for iperf3 checks (e.g. timing, port, username, -password, rsa_publc_key etc) using the :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -<#openwisp_monitoring_iperf3_check_config>`_ setting. +.. note:: -**Note:** When setting :ref:`OPENWISP_MONITORING_AUTO_IPERF3 ` to ``True``, -you may need to update the :ref:`metric configuration ` -to enable alerts for the iperf3 check. + When setting :ref:`openwisp_monitoring_auto_iperf3` to ``True``, you + may need to update the :ref:`metric configuration ` to enable alerts for the iperf3 + check. diff --git a/docs/user/dashboard-monitoring-charts.rst b/docs/user/dashboard-monitoring-charts.rst index c890f6be..866d7ed3 100644 --- a/docs/user/dashboard-monitoring-charts.rst +++ b/docs/user/dashboard-monitoring-charts.rst @@ -1,15 +1,15 @@ Dashboard Monitoring Charts ---------------------------- +=========================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/dashboard-charts.png - :align: center + :align: center OpenWISP Monitoring adds two timeseries charts to the admin dashboard: -- **General WiFi clients Chart**: Shows the number of connected clients to the WiFi - interfaces of devices in the network. -- **General traffic Chart**: Shows the amount of traffic flowing in the network. +- **General WiFi clients Chart**: Shows the number of connected clients to + the WiFi interfaces of devices in the network. +- **General traffic Chart**: Shows the amount of traffic flowing in the + network. -You can configure the interfaces included in the **General traffic chart** using -the :ref:`"OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART" -<#openwisp_monitoring_dashboard_traffic_chart>`_ setting. +You can configure the interfaces included in the **General traffic chart** +using the :ref:`openwisp_monitoring_dashboard_traffic_chart` setting. diff --git a/docs/user/default-alerts-and-notifications.rst b/docs/user/default-alerts-and-notifications.rst index c3117db7..41090641 100644 --- a/docs/user/default-alerts-and-notifications.rst +++ b/docs/user/default-alerts-and-notifications.rst @@ -1,17 +1,17 @@ Default Alerts / Notifications ------------------------------- +============================== -+-------------------------------+------------------------------------------------------------------+ -| Notification Type | Use | -+-------------------------------+------------------------------------------------------------------+ -| ``threshold_crossed`` | Fires when a metric crosses the boundary defined in the | -| | threshold value of the alert settings. | -+-------------------------------+------------------------------------------------------------------+ -| ``threshold_recovery`` | Fires when a metric goes back within the expected range. | -+-------------------------------+------------------------------------------------------------------+ -| ``connection_is_working`` | Fires when the connection to a device is working. | -+-------------------------------+------------------------------------------------------------------+ -| ``connection_is_not_working`` | Fires when the connection (eg: SSH) to a device stops working | -| | (eg: credentials are outdated, management IP address is | -| | outdated, or device is not reachable). | -+-------------------------------+------------------------------------------------------------------+ +============================= ============================================ +Notification Type Use +``threshold_crossed`` Fires when a metric crosses the boundary + defined in the threshold value of the alert + settings. +``threshold_recovery`` Fires when a metric goes back within the + expected range. +``connection_is_working`` Fires when the connection to a device is + working. +``connection_is_not_working`` Fires when the connection (eg: SSH) to a + device stops working (eg: credentials are + outdated, management IP address is outdated, + or device is not reachable). +============================= ============================================ diff --git a/docs/user/default-metrics.rst b/docs/user/default-metrics.rst index cba6abf6..91611c2d 100644 --- a/docs/user/default-metrics.rst +++ b/docs/user/default-metrics.rst @@ -1,267 +1,238 @@ Default Metrics ---------------- +=============== Device Status -~~~~~~~~~~~~~ +------------- This metric stores the status of the device for viewing purposes. .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-1.png - :align: center + :align: center .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-2.png - :align: center + :align: center .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-3.png - :align: center + :align: center .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-4.png - :align: center + :align: center Ping -~~~~ - -+--------------------+----------------------------------------------------------------+ -| **measurement**: | ``ping`` | -+--------------------+----------------------------------------------------------------+ -| **types**: | ``int`` (reachable and loss), ``float`` (rtt) | -+--------------------+----------------------------------------------------------------+ -| **fields**: | ``reachable``, ``loss``, ``rtt_min``, ``rtt_max``, ``rtt_avg`` | -+--------------------+----------------------------------------------------------------+ -| **configuration**: | ``ping`` | -+--------------------+----------------------------------------------------------------+ -| **charts**: | ``uptime``, ``packet_loss``, ``rtt`` | -+--------------------+----------------------------------------------------------------+ +---- + +================== ================================================== +**measurement**: ``ping`` +**types**: ``int`` (reachable and loss), ``float`` (rtt) +**fields**: ``reachable``, ``loss``, ``rtt_min``, ``rtt_max``, + ``rtt_avg`` +**configuration**: ``ping`` +**charts**: ``uptime``, ``packet_loss``, ``rtt`` +================== ================================================== **Uptime**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/uptime.png - :align: center + :align: center **Packet loss**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/packet-loss.png - :align: center + :align: center **Round Trip Time**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/rtt.png - :align: center + :align: center Traffic -~~~~~~~ - -+--------------------+--------------------------------------------------------------------------+ -| **measurement**: | ``traffic`` | -+--------------------+--------------------------------------------------------------------------+ -| **type**: | ``int`` | -+--------------------+--------------------------------------------------------------------------+ -| **fields**: | ``rx_bytes``, ``tx_bytes`` | -+--------------------+--------------------------------------------------------------------------+ -| **tags**: | .. code-block:: python | -| | | -| | { | -| | 'organization_id': '', | -| | 'ifname': '', | -| | # optional | -| | 'location_id': '', | -| | 'floorplan_id': '', | -| | } | -+--------------------+--------------------------------------------------------------------------+ -| **configuration**: | ``traffic`` | -+--------------------+--------------------------------------------------------------------------+ -| **charts**: | ``traffic`` | -+--------------------+--------------------------------------------------------------------------+ +------- + +================== ========================================================================== +**measurement**: ``traffic`` +**type**: ``int`` +**fields**: ``rx_bytes``, ``tx_bytes`` +**tags**: .. code-block:: python + + { + "organization_id": "", + "ifname": "", + # optional + "location_id": "", + "floorplan_id": "", + } +**configuration**: ``traffic`` +**charts**: ``traffic`` +================== ========================================================================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/traffic.png - :align: center + :align: center WiFi Clients -~~~~~~~~~~~~ - -+--------------------+--------------------------------------------------------------------------+ -| **measurement**: | ``wifi_clients`` | -+--------------------+--------------------------------------------------------------------------+ -| **type**: | ``int`` | -+--------------------+--------------------------------------------------------------------------+ -| **fields**: | ``clients`` | -+--------------------+--------------------------------------------------------------------------+ -| **tags**: | .. code-block:: python | -| | | -| | { | -| | 'organization_id': '', | -| | 'ifname': '', | -| | # optional | -| | 'location_id': '', | -| | 'floorplan_id': '', | -| | } | -+--------------------+--------------------------------------------------------------------------+ -| **configuration**: | ``clients`` | -+--------------------+--------------------------------------------------------------------------+ -| **charts**: | ``wifi_clients`` | -+--------------------+--------------------------------------------------------------------------+ - +------------ + +================== ========================================================================== +**measurement**: ``wifi_clients`` +**type**: ``int`` +**fields**: ``clients`` +**tags**: .. code-block:: python + + { + "organization_id": "", + "ifname": "", + # optional + "location_id": "", + "floorplan_id": "", + } +**configuration**: ``clients`` +**charts**: ``wifi_clients`` +================== ========================================================================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-clients.png - :align: center + :align: center Memory Usage -~~~~~~~~~~~~ - -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **measurement**: | ```` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **type**: | ``float`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **fields**: | ``percent_used``, ``free_memory``, ``total_memory``, ``buffered_memory``, ``shared_memory``, ``cached_memory``, ``available_memory`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **configuration**: | ``memory`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| **charts**: | ``memory`` | -+--------------------+--------------------------------------------------------------------------------------------------------------------------------------+ +------------ + +================== ==================================================== +**measurement**: ```` +**type**: ``float`` +**fields**: ``percent_used``, ``free_memory``, ``total_memory``, + ``buffered_memory``, ``shared_memory``, + ``cached_memory``, ``available_memory`` +**configuration**: ``memory`` +**charts**: ``memory`` +================== ==================================================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/memory.png - :align: center + :align: center CPU Load -~~~~~~~~ - -+--------------------+----------------------------------------------------+ -| **measurement**: | ``load`` | -+--------------------+----------------------------------------------------+ -| **type**: | ``float`` | -+--------------------+----------------------------------------------------+ -| **fields**: | ``cpu_usage``, ``load_1``, ``load_5``, ``load_15`` | -+--------------------+----------------------------------------------------+ -| **configuration**: | ``load`` | -+--------------------+----------------------------------------------------+ -| **charts**: | ``load`` | -+--------------------+----------------------------------------------------+ +-------- + +================== ================================================== +**measurement**: ``load`` +**type**: ``float`` +**fields**: ``cpu_usage``, ``load_1``, ``load_5``, ``load_15`` +**configuration**: ``load`` +**charts**: ``load`` +================== ================================================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/cpu-load.png - :align: center + :align: center Disk Usage -~~~~~~~~~~ - -+--------------------+-------------------+ -| **measurement**: | ``disk`` | -+--------------------+-------------------+ -| **type**: | ``float`` | -+--------------------+-------------------+ -| **fields**: | ``used_disk`` | -+--------------------+-------------------+ -| **configuration**: | ``disk`` | -+--------------------+-------------------+ -| **charts**: | ``disk`` | -+--------------------+-------------------+ +---------- + +================== ============= +**measurement**: ``disk`` +**type**: ``float`` +**fields**: ``used_disk`` +**configuration**: ``disk`` +**charts**: ``disk`` +================== ============= .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/disk-usage.png - :align: center + :align: center Mobile Signal Strength -~~~~~~~~~~~~~~~~~~~~~~ - -+--------------------+-----------------------------------------+ -| **measurement**: | ``signal_strength`` | -+--------------------+-----------------------------------------+ -| **type**: | ``float`` | -+--------------------+-----------------------------------------+ -| **fields**: | ``signal_strength``, ``signal_power`` | -+--------------------+-----------------------------------------+ -| **configuration**: | ``signal_strength`` | -+--------------------+-----------------------------------------+ -| **charts**: | ``signal_strength`` | -+--------------------+-----------------------------------------+ +---------------------- + +================== ===================================== +**measurement**: ``signal_strength`` +**type**: ``float`` +**fields**: ``signal_strength``, ``signal_power`` +**configuration**: ``signal_strength`` +**charts**: ``signal_strength`` +================== ===================================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-strength.png - :align: center + :align: center Mobile Signal Quality -~~~~~~~~~~~~~~~~~~~~~~ - -+--------------------+-----------------------------------------+ -| **measurement**: | ``signal_quality`` | -+--------------------+-----------------------------------------+ -| **type**: | ``float`` | -+--------------------+-----------------------------------------+ -| **fields**: | ``signal_quality``, ``signal_quality`` | -+--------------------+-----------------------------------------+ -| **configuration**: | ``signal_quality`` | -+--------------------+-----------------------------------------+ -| **charts**: | ``signal_quality`` | -+--------------------+-----------------------------------------+ +--------------------- + +================== ====================================== +**measurement**: ``signal_quality`` +**type**: ``float`` +**fields**: ``signal_quality``, ``signal_quality`` +**configuration**: ``signal_quality`` +**charts**: ``signal_quality`` +================== ====================================== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-quality.png - :align: center + :align: center Mobile Access Technology in use -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------------+-------------------+ -| **measurement**: | ``access_tech`` | -+--------------------+-------------------+ -| **type**: | ``int`` | -+--------------------+-------------------+ -| **fields**: | ``access_tech`` | -+--------------------+-------------------+ -| **configuration**: | ``access_tech`` | -+--------------------+-------------------+ -| **charts**: | ``access_tech`` | -+--------------------+-------------------+ +------------------------------- + +================== =============== +**measurement**: ``access_tech`` +**type**: ``int`` +**fields**: ``access_tech`` +**configuration**: ``access_tech`` +**charts**: ``access_tech`` +================== =============== .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/access-technology.png - :align: center + :align: center Iperf3 -~~~~~~ - -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **measurement**: | ``iperf3`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **types**: | | ``int`` (iperf3_result, sent_bytes_tcp, received_bytes_tcp, retransmits, sent_bytes_udp, total_packets, lost_packets), | -| | | ``float`` (sent_bps_tcp, received_bps_tcp, sent_bps_udp, jitter, lost_percent) | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **fields**: | | ``iperf3_result``, ``sent_bps_tcp``, ``received_bps_tcp``, ``sent_bytes_tcp``, ``received_bytes_tcp``, ``retransmits``, | -| | | ``sent_bps_udp``, ``sent_bytes_udp``, ``jitter``, ``total_packets``, ``lost_packets``, ``lost_percent`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **configuration**: | ``iperf3`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ -| **charts**: | ``bandwidth``, ``transfer``, ``retransmits``, ``jitter``, ``datagram``, ``datagram_loss`` | -+--------------------+---------------------------------------------------------------------------------------------------------------------------+ +------ + +================== ===================================================== +**measurement**: ``iperf3`` +**types**: | ``int`` (iperf3_result, sent_bytes_tcp, + received_bytes_tcp, retransmits, sent_bytes_udp, + total_packets, lost_packets), + | ``float`` (sent_bps_tcp, received_bps_tcp, + sent_bps_udp, jitter, lost_percent) +**fields**: | ``iperf3_result``, ``sent_bps_tcp``, + ``received_bps_tcp``, ``sent_bytes_tcp``, + ``received_bytes_tcp``, ``retransmits``, + | ``sent_bps_udp``, ``sent_bytes_udp``, ``jitter``, + ``total_packets``, ``lost_packets``, + ``lost_percent`` +**configuration**: ``iperf3`` +**charts**: ``bandwidth``, ``transfer``, ``retransmits``, + ``jitter``, ``datagram``, ``datagram_loss`` +================== ===================================================== **Bandwidth**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/bandwidth.png - :align: center + :align: center **Transferred Data**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/transferred-data.png - :align: center + :align: center **Retransmits**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/retransmits.png - :align: center + :align: center **Jitter**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/jitter.png - :align: center + :align: center **Datagram**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram.png - :align: center + :align: center **Datagram loss**: .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram-loss.png - :align: center + :align: center + +For more info on how to configure and use Iperf3, please refer to `iperf3 +check usage instructions <#iperf3-check-usage-instructions>`_. -For more info on how to configure and use Iperf3, please refer to -`iperf3 check usage instructions <#iperf3-check-usage-instructions>`_. +.. note:: -**Note:** Iperf3 charts uses ``connect_points=True`` in -:ref:`default chart configuration ` that joins it's individual chart data points. + Iperf3 charts uses ``connect_points=True`` in :ref:`default chart + configuration ` that joins it's individual + chart data points. diff --git a/docs/user/device-health-status.rst b/docs/user/device-health-status.rst index edab4d8f..7dfd4e4c 100644 --- a/docs/user/device-health-status.rst +++ b/docs/user/device-health-status.rst @@ -1,35 +1,38 @@ Device Health Status --------------------- +==================== -The possible values for the health status field (``DeviceMonitoring.status``) -are explained below. +The possible values for the health status field +(``DeviceMonitoring.status``) are explained below. ``UNKNOWN`` -~~~~~~~~~~~ +----------- -Whenever a new device is created it will have ``UNKNOWN`` as it's default Heath Status. +Whenever a new device is created it will have ``UNKNOWN`` as it's default +Heath Status. -It implies that the system doesn't know whether the device is reachable yet. +It implies that the system doesn't know whether the device is reachable +yet. ``OK`` -~~~~~~ +------ Everything is working normally. ``PROBLEM`` -~~~~~~~~~~~ +----------- -One of the metrics has a value which is not in the expected range -(the threshold value set in the alert settings has been crossed). +One of the metrics has a value which is not in the expected range (the +threshold value set in the alert settings has been crossed). Example: CPU usage should be less than 90% but current value is at 95%. ``CRITICAL`` -~~~~~~~~~~~~ +------------ -One of the metrics defined in ``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` -has a value which is not in the expected range -(the threshold value set in the alert settings has been crossed). +One of the metrics defined in +``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` has a value which is not +in the expected range (the threshold value set in the alert settings has +been crossed). -Example: ping is by default a critical metric which is expected to be always 1 -(reachable). +Example: ping is by default a critical metric which is expected to be +always 1 (reachable). diff --git a/docs/user/iperf3-usage-instructions.rst b/docs/user/iperf3-usage-instructions.rst index bef6395e..2d478879 100644 --- a/docs/user/iperf3-usage-instructions.rst +++ b/docs/user/iperf3-usage-instructions.rst @@ -1,12 +1,12 @@ Iperf3 Check Usage Instructions -------------------------------- +=============================== 1. Make sure iperf3 is installed on the device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------- Register your device to OpenWISP and make sure the `iperf3 openwrt package -`_ is installed on the device, -eg: +`_ is installed on the +device, eg: .. code-block:: shell @@ -14,26 +14,28 @@ eg: opkg install iperf3-ssl # if using with authentication (read below for more info) 2. Ensure SSH access from OpenWISP is enabled on your devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------------------- Follow the steps in `"How to configure push updates" section of the OpenWISP documentation -`_ -to allow SSH access to you device from OpenWISP. +`_ to allow SSH +access to you device from OpenWISP. -**Note:** Make sure device connection is enabled -& working with right update strategy i.e. ``OpenWRT SSH``. +.. note:: + + Make sure device connection is enabled & working with right update + strategy i.e. ``OpenWRT SSH``. .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png - :alt: Enable ssh access from openwisp to device - :align: center + :alt: Enable ssh access from openwisp to device + :align: center 3. Set up and configure Iperf3 server settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------- -After having deployed your Iperf3 servers, you need to -configure the iperf3 settings on the django side of OpenWISP, -see the `test project settings for reference +After having deployed your Iperf3 servers, you need to configure the +iperf3 settings on the django side of OpenWISP, see the `test project +settings for reference `_. The host can be specified by hostname, IPv4 literal, or IPv6 literal. @@ -41,33 +43,36 @@ Example: .. code-block:: python - OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { - # 'org_pk' : {'host' : [], 'client_options' : {}} - 'a9734710-db30-46b0-a2fc-01f01046fe4f': { - # Some public iperf3 servers - # https://iperf.fr/iperf-servers.php#public-servers - 'host': ['iperf3.openwisp.io', '2001:db8::1', '192.168.5.2'], - 'client_options': { - 'port': 5209, - 'udp': {'bitrate': '30M'}, - 'tcp': {'bitrate': '0'}, - }, - }, - # another org - 'b9734710-db30-46b0-a2fc-01f01046fe4f': { - # available iperf3 servers - 'host': ['iperf3.openwisp2.io', '192.168.5.3'], - 'client_options': { - 'port': 5207, - 'udp': {'bitrate': '50M'}, - 'tcp': {'bitrate': '20M'}, - }, - }, - } - -**Note:** If an organization has more than one iperf3 server configured, then it enables -the iperf3 checks to run concurrently on different devices. If all of the available servers -are busy, then it will add the check back in the queue. + OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { + # 'org_pk' : {'host' : [], 'client_options' : {}} + "a9734710-db30-46b0-a2fc-01f01046fe4f": { + # Some public iperf3 servers + # https://iperf.fr/iperf-servers.php#public-servers + "host": ["iperf3.openwisp.io", "2001:db8::1", "192.168.5.2"], + "client_options": { + "port": 5209, + "udp": {"bitrate": "30M"}, + "tcp": {"bitrate": "0"}, + }, + }, + # another org + "b9734710-db30-46b0-a2fc-01f01046fe4f": { + # available iperf3 servers + "host": ["iperf3.openwisp2.io", "192.168.5.3"], + "client_options": { + "port": 5207, + "udp": {"bitrate": "50M"}, + "tcp": {"bitrate": "20M"}, + }, + }, + } + +.. note:: + + If an organization has more than one iperf3 server configured, then it + enables the iperf3 checks to run concurrently on different devices. If + all of the available servers are busy, then it will add the check back + in the queue. The celery-beat configuration for the iperf3 check needs to be added too: @@ -81,157 +86,160 @@ The celery-beat configuration for the iperf3 check needs to be added too: CELERY_BEAT_SCHEDULE = { # Other celery beat configurations # Celery beat configuration for iperf3 check - 'run_iperf3_checks': { - 'task': 'openwisp_monitoring.check.tasks.run_checks', + "run_iperf3_checks": { + "task": "openwisp_monitoring.check.tasks.run_checks", # https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html#crontab-schedules # Executes check every 5 mins from 00:00 AM to 6:00 AM (night) - 'schedule': crontab(minute='*/5', hour='0-6'), + "schedule": crontab(minute="*/5", hour="0-6"), # Iperf3 check path - 'args': (['openwisp_monitoring.check.classes.Iperf3'],), - 'relative': True, + "args": (["openwisp_monitoring.check.classes.Iperf3"],), + "relative": True, } } Once the changes are saved, you will need to restart all the processes. -**Note:** We recommended to configure this check to run in non peak -traffic times to not interfere with standard traffic. +.. note:: + + We recommended to configure this check to run in non peak traffic + times to not interfere with standard traffic. 4. Run the check -~~~~~~~~~~~~~~~~ +---------------- This should happen automatically if you have celery-beat correctly -configured and running in the background. -For testing purposes, you can run this check manually using the -:ref:`run_checks ` command. +configured and running in the background. For testing purposes, you can +run this check manually using the :ref:`run_checks ` command. After that, you should see the iperf3 network measurements charts. .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png - :alt: Iperf3 network measurement charts + :alt: Iperf3 network measurement charts Iperf3 check parameters -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- Currently, iperf3 check supports the following parameters: -+-----------------------+----------+--------------------------------------------------------------------+ -| **Parameter** | **Type** | **Default Value** | -+-----------------------+----------+--------------------------------------------------------------------+ -|``host`` | ``list`` | ``[]`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``username`` | ``str`` | ``''`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``password`` | ``str`` | ``''`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``rsa_public_key`` | ``str`` | ``''`` | -+-----------------------+----------+--------------------------------------------------------------------+ -|``client_options`` | +---------------------+----------+------------------------------------------+ | -| | | **Parameters** | **Type** | **Default Value** | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``port`` | ``int`` | ``5201`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``time`` | ``int`` | ``10`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``bytes`` | ``str`` | ``''`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``blockcount`` | ``str`` | ``''`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``window`` | ``str`` | ``0`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``parallel`` | ``int`` | ``1`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``reverse`` | ``bool`` | ``False`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``bidirectional`` | ``bool`` | ``False`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``connect_timeout`` | ``int`` | ``1000`` | | -| | +---------------------+----------+------------------------------------------+ | -| | | ``tcp`` | +----------------+----------+---------------------+ | | -| | | | | **Parameters** | **Type** | **Default Value** | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``bitrate`` | ``str`` | ``0`` | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``length`` | ``str`` | ``128K`` | | | -| | | | +----------------+----------+---------------------+ | | -| | +---------------------+-----------------------------------------------------+ | -| | | ``udp`` | +----------------+----------+---------------------+ | | -| | | | | **Parameters** | **Type** | **Default Value** | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``bitrate`` | ``str`` | ``30M`` | | | -| | | | +----------------+----------+---------------------+ | | -| | | | |``length`` | ``str`` | ``0`` | | | -| | | | +----------------+----------+---------------------+ | | -| | +---------------------+-----------------------------------------------------+ | -+-----------------------+-------------------------------------------------------------------------------+ - -To learn how to use these parameters, please see the -:ref:`iperf3 check configuration example `. - -Visit the `official documentation `_ -to learn more about the iperf3 parameters. +================== ======== ========================================= +**Parameter** **Type** **Default Value** +``host`` ``list`` ``[]`` +``username`` ``str`` ``''`` +``password`` ``str`` ``''`` +``rsa_public_key`` ``str`` ``''`` +``client_options`` ``dict`` Refer the :ref:`iperf3_client_parameters` + table below for available parameters +================== ======== ========================================= -Iperf3 authentication +.. _iperf3_client_parameters: + +Iperf3 client options ~~~~~~~~~~~~~~~~~~~~~ -By default iperf3 check runs without any kind of **authentication**, -in this section we will explain how to configure **RSA authentication** -between the **client** and the **server** to restrict connections -to authenticated clients. +=================== ======== ========================================== +**Parameters** **Type** **Default Value** +``port`` ``int`` ``5201`` +``time`` ``int`` ``10`` +``bytes`` ``str`` ``''`` +``blockcount`` ``str`` ``''`` +``window`` ``str`` ``0`` +``parallel`` ``int`` ``1`` +``reverse`` ``bool`` ``False`` +``bidirectional`` ``bool`` ``False`` +``connect_timeout`` ``int`` ``1000`` +``tcp`` ``dict`` Refer the :ref:`iperf3_client_tcp_options` + table below for available parameters +``udp`` ``dict`` Refer the :ref:`iperf3_client_udp_options` + table below for available parameters +=================== ======== ========================================== + +.. _iperf3_client_tcp_options: + +Iperf3 client's TCP options ++++++++++++++++++++++++++++ + +============== ======== ================= +**Parameters** **Type** **Default Value** +``bitrate`` ``str`` ``0`` +``length`` ``str`` ``128K`` +============== ======== ================= + +.. _iperf3_client_udp_options: + +Iperf3 client's UDP options ++++++++++++++++++++++++++++ + +============== ======== ================= +**Parameters** **Type** **Default Value** +``bitrate`` ``str`` ``30M`` +``length`` ``str`` ``0`` +============== ======== ================= + +To learn how to use these parameters, please see the :ref:`iperf3 check +configuration example `. + +Visit the `official documentation `_ to +learn more about the iperf3 parameters. + +Iperf3 authentication +--------------------- + +By default iperf3 check runs without any kind of **authentication**, in +this section we will explain how to configure **RSA authentication** +between the **client** and the **server** to restrict connections to +authenticated clients. Server side -########### +~~~~~~~~~~~ 1. Generate RSA keypair -^^^^^^^^^^^^^^^^^^^^^^^ ++++++++++++++++++++++++ .. code-block:: shell - openssl genrsa -des3 -out private.pem 2048 - openssl rsa -in private.pem -outform PEM -pubout -out public_key.pem - openssl rsa -in private.pem -out private_key.pem -outform PEM + openssl genrsa -des3 -out private.pem 2048 + openssl rsa -in private.pem -outform PEM -pubout -out public_key.pem + openssl rsa -in private.pem -out private_key.pem -outform PEM -After running the commands mentioned above, the public key will be stored in -``public_key.pem`` which will be used in **rsa_public_key** parameter +After running the commands mentioned above, the public key will be stored +in ``public_key.pem`` which will be used in **rsa_public_key** parameter in :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -` -and the private key will be contained in the file ``private_key.pem`` -which will be used with **--rsa-private-key-path** command option when -starting the iperf3 server. +` and the private key will be +contained in the file ``private_key.pem`` which will be used with +**--rsa-private-key-path** command option when starting the iperf3 server. 2. Create user credentials -^^^^^^^^^^^^^^^^^^^^^^^^^^ +++++++++++++++++++++++++++ .. code-block:: shell - USER=iperfuser PASSWD=iperfpass - echo -n "{$USER}$PASSWD" | sha256sum | awk '{ print $1 }' - ---- - ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 #This is the hash of "iperfuser" + USER=iperfuser PASSWD=iperfpass + echo -n "{$USER}$PASSWD" | sha256sum | awk '{ print $1 }' + ---- + ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 #This is the hash of "iperfuser" Add the above hash with username in ``credentials.csv`` .. code-block:: shell - # file format: username,sha256 - iperfuser,ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 + # file format: username,sha256 + iperfuser,ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 3. Now start the iperf3 server with auth options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +++++++++++++++++++++++++++++++++++++++++++++++++ .. code-block:: shell - iperf3 -s --rsa-private-key-path ./private_key.pem --authorized-users-path ./credentials.csv + iperf3 -s --rsa-private-key-path ./private_key.pem --authorized-users-path ./credentials.csv Client side (OpenWrt device) -############################ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. Install iperf3-ssl -^^^^^^^^^^^^^^^^^^^^^ ++++++++++++++++++++++ Install the `iperf3-ssl openwrt package -`_ -instead of the normal +`_ instead of the normal `iperf3 openwrt package `_ because the latter comes without support for authentication. @@ -239,48 +247,51 @@ You may also check your installed **iperf3 openwrt package** features: .. code-block:: shell - root@vm-openwrt:~ iperf3 -v - iperf 3.7 (cJSON 1.5.2) - Linux vm-openwrt 4.14.171 #0 SMP Thu Feb 27 21:05:12 2020 x86_64 - Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, - sendfile / zerocopy, socket pacing, authentication # contains 'authentication' + root@vm-openwrt:- iperf3 -v + iperf 3.7 (cJSON 1.5.2) + Linux vm-openwrt 4.14.171 #0 SMP Thu Feb 27 21:05:12 2020 x86_64 + Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, + sendfile / zerocopy, socket pacing, authentication # contains 'authentication' .. _configure-iperf3-check-auth-parameters: 2. Configure iperf3 check auth parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++++++++++++++++++++++++++++++++++++++++++ -Now, add the following iperf3 authentication parameters -to :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -` -in the settings: +Now, add the following iperf3 authentication parameters to +:ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG +` in the settings: .. code-block:: python - OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { - 'a9734710-db30-46b0-a2fc-01f01046fe4f': { - 'host': ['iperf1.openwisp.io', 'iperf2.openwisp.io', '192.168.5.2'], - # All three parameters (username, password, rsa_publc_key) - # are required for iperf3 authentication - 'username': 'iperfuser', - 'password': 'iperfpass', - # Add RSA public key without any headers - # ie. -----BEGIN PUBLIC KEY-----, -----BEGIN END KEY----- - 'rsa_public_key': ( - """ - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwuEm+iYrfSWJOupy6X3N - dxZvUCxvmoL3uoGAs0O0Y32unUQrwcTIxudy38JSuCccD+k2Rf8S4WuZSiTxaoea - 6Du99YQGVZeY67uJ21SWFqWU+w6ONUj3TrNNWoICN7BXGLE2BbSBz9YaXefE3aqw - GhEjQz364Itwm425vHn2MntSp0weWb4hUCjQUyyooRXPrFUGBOuY+VvAvMyAG4Uk - msapnWnBSxXt7Tbb++A5XbOMdM2mwNYDEtkD5ksC/x3EVBrI9FvENsH9+u/8J9Mf - 2oPl4MnlCMY86MQypkeUn7eVWfDnseNky7TyC0/IgCXve/iaydCCFdkjyo1MTAA4 - BQIDAQAB - """ - ), - 'client_options': { - 'port': 5209, - 'udp': {'bitrate': '20M'}, - 'tcp': {'bitrate': '0'}, - }, - } - } + OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { + "a9734710-db30-46b0-a2fc-01f01046fe4f": { + "host": [ + "iperf1.openwisp.io", + "iperf2.openwisp.io", + "192.168.5.2", + ], + # All three parameters (username, password, rsa_publc_key) + # are required for iperf3 authentication + "username": "iperfuser", + "password": "iperfpass", + # Add RSA public key without any headers + # ie. -----BEGIN PUBLIC KEY-----, -----BEGIN END KEY----- + "rsa_public_key": ( + """ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwuEm+iYrfSWJOupy6X3N + dxZvUCxvmoL3uoGAs0O0Y32unUQrwcTIxudy38JSuCccD+k2Rf8S4WuZSiTxaoea + 6Du99YQGVZeY67uJ21SWFqWU+w6ONUj3TrNNWoICN7BXGLE2BbSBz9YaXefE3aqw + GhEjQz364Itwm425vHn2MntSp0weWb4hUCjQUyyooRXPrFUGBOuY+VvAvMyAG4Uk + msapnWnBSxXt7Tbb++A5XbOMdM2mwNYDEtkD5ksC/x3EVBrI9FvENsH9+u/8J9Mf + 2oPl4MnlCMY86MQypkeUn7eVWfDnseNky7TyC0/IgCXve/iaydCCFdkjyo1MTAA4 + BQIDAQAB + """ + ), + "client_options": { + "port": 5209, + "udp": {"bitrate": "20M"}, + "tcp": {"bitrate": "0"}, + }, + } + } diff --git a/docs/user/passive-vs-active-metric-collection.rst b/docs/user/passive-vs-active-metric-collection.rst index 64fd999d..ba681d24 100644 --- a/docs/user/passive-vs-active-metric-collection.rst +++ b/docs/user/passive-vs-active-metric-collection.rst @@ -1,19 +1,18 @@ Passive vs Active Metric Collection ------------------------------------ +=================================== The `the different device metric `_ collected by OpenWISP Monitoring can be divided in two categories: -1. **metrics collected actively by OpenWISP**: - these metrics are collected by the celery workers running on the - OpenWISP server, which continuously sends network requests to the - devices and store the results; -2. **metrics collected passively by OpenWISP**: - these metrics are sent by the - `openwrt-openwisp-monitoring agent <#install-monitoring-packages-on-the-device>`_ - installed on the network devices and are collected by OpenWISP via - its REST API. +1. **metrics collected actively by OpenWISP**: these metrics are collected + by the celery workers running on the OpenWISP server, which + continuously sends network requests to the devices and store the + results; +2. **metrics collected passively by OpenWISP**: these metrics are sent by + the `openwrt-openwisp-monitoring agent + <#install-monitoring-packages-on-the-device>`_ installed on the network + devices and are collected by OpenWISP via its REST API. The `"Available Checks" <#available-checks>`_ section of this document lists the currently implemented **active checks**. diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 05b372a2..9fe4b810 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,109 +1,109 @@ Quickstart Guide ----------------- +================ Install OpenWISP Monitoring -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- Install *OpenWISP Monitoring* using one of the methods mentioned in the `"Installation instructions" <#installation-instructions>`_. Install openwisp-config on the device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------- `Install the openwisp-config agent for OpenWrt `_ on your device. Install monitoring packages on the device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------- `Install the openwrt-openwisp-monitoring packages `_ on your device. -These packages collect and send the -monitoring data from the device to OpenWISP Monitoring and -are required to collect :ref:`metrics ` -like interface traffic, WiFi clients, CPU load, memory usage, etc. +These packages collect and send the monitoring data from the device to +OpenWISP Monitoring and are required to collect :ref:`metrics +` like interface traffic, WiFi clients, CPU +load, memory usage, etc. -**Note**: if you are an existing user of *openwisp-monitoring* and are using -the legacy *monitoring template* for collecting metrics, we highly recommend -`Migrating from monitoring scripts to monitoring packages -<#migrating-from-monitoring-scripts-to-monitoring-packages>`_. +.. note:: + + If you are an existing user of *openwisp-monitoring* and are using the + legacy *monitoring template* for collecting metrics, we highly + recommend `Migrating from monitoring scripts to monitoring packages + <#migrating-from-monitoring-scripts-to-monitoring-packages>`_. Make sure OpenWISP can reach your devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------- -In order to perform `active checks <#available-checks>`_ and other actions like -`triggering the push of configuration changes +In order to perform `active checks <#available-checks>`_ and other actions +like `triggering the push of configuration changes `_, `executing shell commands -`_ or -`performing firmware upgrades +`_ +or `performing firmware upgrades `_, **the OpenWISP server needs to be able to reach the network devices**. There are mainly two deployment scenarios for OpenWISP: -1. the OpenWISP server is deployed on the public internet and the devices are - geographically distributed across different locations: - **in this case a management tunnel is needed** -2. the OpenWISP server is deployed on a computer/server which is located in - the same Layer 2 network (that is, in the same LAN) where the devices - are located. - **in this case a management tunnel is NOT needed** +1. the OpenWISP server is deployed on the public internet and the devices + are geographically distributed across different locations: **in this + case a management tunnel is needed** +2. the OpenWISP server is deployed on a computer/server which is located + in the same Layer 2 network (that is, in the same LAN) where the + devices are located. **in this case a management tunnel is NOT needed** 1. Public internet deployment -############################# +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is the most common scenario: -- the OpenWISP server is deployed to the public internet, hence the - server has a public IPv4 (and IPv6) address and usually a valid - SSL certificate provided by Mozilla Letsencrypt or another SSL provider +- the OpenWISP server is deployed to the public internet, hence the server + has a public IPv4 (and IPv6) address and usually a valid SSL certificate + provided by Mozilla Letsencrypt or another SSL provider - the network devices are geographically distributed across different locations (different cities, different regions, different countries) In this scenario, the OpenWISP application will not be able to reach the -devices **unless a management tunnel** is used, for that reason having -a management VPN like OpenVPN, Wireguard or any other tunneling solution -is paramount, not only to allow OpenWISP to work properly, but also to -be able to perform debugging and troubleshooting when needed. +devices **unless a management tunnel** is used, for that reason having a +management VPN like OpenVPN, Wireguard or any other tunneling solution is +paramount, not only to allow OpenWISP to work properly, but also to be +able to perform debugging and troubleshooting when needed. In this scenario, the following requirements are needed: -- a VPN server must be installed in a way that the OpenWISP - server can reach the VPN peers, for more information on how to do this - via OpenWISP please refer to the following sections: +- a VPN server must be installed in a way that the OpenWISP server can + reach the VPN peers, for more information on how to do this via OpenWISP + please refer to the following sections: - - `OpenVPN tunnel automation - `_ + - `OpenVPN tunnel automation `_ - `Wireguard tunnel automation `_ If you prefer to use other tunneling solutions (L2TP, Softether, etc.) - and know how to configure those solutions on your own, - that's totally fine as well. - - If the OpenWISP server is connected to a network infrastructure - which allows it to reach the devices via pre-existing tunneling or - Intranet solutions (eg: MPLS, SD-WAN), then setting up a VPN server - is not needed, as long as there's a dedicated interface on OpenWrt - which gets an IP address assigned to it and which is reachable from - the OpenWISP server. - -- The devices must be configured to join the management tunnel automatically, - either via a pre-existing configuration in the firmware or via an - `OpenWISP Template `_. - + and know how to configure those solutions on your own, that's totally + fine as well. + + If the OpenWISP server is connected to a network infrastructure which + allows it to reach the devices via pre-existing tunneling or Intranet + solutions (eg: MPLS, SD-WAN), then setting up a VPN server is not + needed, as long as there's a dedicated interface on OpenWrt which gets + an IP address assigned to it and which is reachable from the OpenWISP + server. + +- The devices must be configured to join the management tunnel + automatically, either via a pre-existing configuration in the firmware + or via an `OpenWISP Template + `_. - The `openwisp-config `_ - agent on the devices must be configured to specify - the ``management_interface`` option, the agent will communicate the - IP of the management interface to the OpenWISP Server and OpenWISP will - use the management IP for reaching the device. + agent on the devices must be configured to specify the + ``management_interface`` option, the agent will communicate the IP of + the management interface to the OpenWISP Server and OpenWISP will use + the management IP for reaching the device. - For example, if the *management interface* is named ``tun0``, - the openwisp-config configuration should look like the following example: + For example, if the *management interface* is named ``tun0``, the + openwisp-config configuration should look like the following example: .. code-block:: text @@ -114,19 +114,19 @@ In this scenario, the following requirements are needed: option management_interface 'tun0' 2. LAN deployment -################# +~~~~~~~~~~~~~~~~~ When the OpenWISP server and the network devices are deployed in the same -L2 network (eg: an office LAN) and the OpenWISP server is reachable -on the LAN address, OpenWISP can then use the **Last IP** field of the -devices to reach them. +L2 network (eg: an office LAN) and the OpenWISP server is reachable on the +LAN address, OpenWISP can then use the **Last IP** field of the devices to +reach them. In this scenario it's necessary to set the -`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" <#openwisp-monitoring-management-ip-only>`_ -setting to ``False``. +`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" +<#openwisp-monitoring-management-ip-only>`_ setting to ``False``. Creating checks for a device -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- By default, the `active checks <#available-checks>`_ are created automatically for all devices, unless the automatic creation of some diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index d99b9894..46528ea2 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -1,49 +1,53 @@ Rest API --------- +======== Live documentation -~~~~~~~~~~~~~~~~~~ +------------------ .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png -A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. +A general live API documentation (following the OpenAPI specification) at +``/api/v1/docs/``. Browsable web interface -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png + .. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png -Additionally, opening any of the endpoints `listed below <#list-of-endpoints>`_ -directly in the browser will show the `browsable API interface of Django-REST-Framework -`_, -which makes it even easier to find out the details of each endpoint. +Additionally, opening any of the endpoints `listed below +<#list-of-endpoints>`_ directly in the browser will show the `browsable +API interface of Django-REST-Framework +`_, which +makes it even easier to find out the details of each endpoint. List of endpoints -~~~~~~~~~~~~~~~~~ +----------------- -Since the detailed explanation is contained in the `Live documentation <#live-documentation>`_ -and in the `Browsable web page <#browsable-web-interface>`_ of each point, -here we'll provide just a list of the available endpoints, -for further information please open the URL of the endpoint in your browser. +Since the detailed explanation is contained in the `Live documentation +<#live-documentation>`_ and in the `Browsable web page +<#browsable-web-interface>`_ of each point, here we'll provide just a list +of the available endpoints, for further information please open the URL of +the endpoint in your browser. Retrieve general monitoring charts -################################## +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/monitoring/dashboard/ This API endpoint is used to show dashboard monitoring charts. It supports -multi-tenancy and allows filtering monitoring data by ``organization_slug``, -``location_id`` and ``floorplan_id`` e.g.: +multi-tenancy and allows filtering monitoring data by +``organization_slug``, ``location_id`` and ``floorplan_id`` e.g.: .. code-block:: text GET /api/v1/monitoring/dashboard/?organization_slug=,&location_id=,&floorplan_id=, -- When retrieving chart data, the ``time`` parameter allows to specify - the time frame, eg: +- When retrieving chart data, the ``time`` parameter allows to specify the + time frame, eg: - ``1d``: returns data of the last day - ``3d``: returns data of the last 3 days @@ -51,34 +55,35 @@ multi-tenancy and allows filtering monitoring data by ``organization_slug``, - ``30d``: returns data of the last 30 days - ``365d``: returns data of the last 365 days -- In alternative to ``time`` it is possible to request chart data for a custom - date range by using the ``start`` and ``end`` parameters, eg: +- In alternative to ``time`` it is possible to request chart data for a + custom date range by using the ``start`` and ``end`` parameters, eg: .. code-block:: text GET /api/v1/monitoring/dashboard/?start={start_datetime}&end={end_datetime} -**Note**: ``start`` and ``end`` parameters should be in the format -``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. +.. note:: + + The ``start`` and ``end`` parameters should be in the format + ``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. Retrieve device charts and device status data -############################################# +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/monitoring/device/{pk}/?key={key}&status=true&time={timeframe} -The format used for Device Status is inspired by -`NetJSON DeviceMonitoring `_. +The format used for Device Status is inspired by `NetJSON DeviceMonitoring +`_. **Notes**: - If the request is made without ``?status=true`` the response will - contain only charts data and will not include any device status information - (current load average, ARP table, DCHP leases, etc.). - -- When retrieving chart data, the ``time`` parameter allows to specify - the time frame, eg: + contain only charts data and will not include any device status + information (current load average, ARP table, DCHP leases, etc.). +- When retrieving chart data, the ``time`` parameter allows to specify the + time frame, eg: - ``1d``: returns data of the last day - ``3d``: returns data of the last 3 days @@ -86,27 +91,27 @@ The format used for Device Status is inspired by - ``30d``: returns data of the last 30 days - ``365d``: returns data of the last 365 days -- In alternative to ``time`` it is possible to request chart data for a custom - date range by using the ``start`` and ``end`` parameters, eg: - -- The response contains device information, monitoring status (health status), - a list of metrics with their respective statuses, chart data and - device status information (only if ``?status=true``). - -- This endpoint can be accessed with session authentication, token authentication, - or alternatively with the device key passed as query string parameter - as shown below (`?key={key}`); - note: this method is meant to be used by the devices. +- In alternative to ``time`` it is possible to request chart data for a + custom date range by using the ``start`` and ``end`` parameters, eg: +- The response contains device information, monitoring status (health + status), a list of metrics with their respective statuses, chart data + and device status information (only if ``?status=true``). +- This endpoint can be accessed with session authentication, token + authentication, or alternatively with the device key passed as query + string parameter as shown below (`?key={key}`); note: this method is + meant to be used by the devices. .. code-block:: text GET /api/v1/monitoring/device/{pk}/?key={key}&status=true&start={start_datetime}&end={end_datetime} -**Note**: ``start`` and ``end`` parameters must be in the format -``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. +.. note:: + + The ``start`` and ``end`` parameters must be in the format + ``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. List device monitoring information -################################## +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -114,83 +119,87 @@ List device monitoring information **Notes**: -- The response contains device information and monitoring status (health status), - but it does not include the information and - health status of the specific metrics, this information - can be retrieved in the detail endpoint of each device. - -- This endpoint can be accessed with session authentication and token authentication. +- The response contains device information and monitoring status (health + status), but it does not include the information and health status of + the specific metrics, this information can be retrieved in the detail + endpoint of each device. +- This endpoint can be accessed with session authentication and token + authentication. **Available filters** -Data can be filtered by health status (e.g. critical, ok, problem, and unknown) -to obtain the list of devices in the corresponding status, for example, -to retrieve the list of devices which are in critical conditions +Data can be filtered by health status (e.g. critical, ok, problem, and +unknown) to obtain the list of devices in the corresponding status, for +example, to retrieve the list of devices which are in critical conditions (eg: unreachable), the following will work: .. code-block:: text - GET /api/v1/monitoring/device/?monitoring__status=critical + GET /api/v1/monitoring/device/?monitoring__status=critical -To filter a list of device monitoring data based -on their organization, you can use the ``organization_id``. +To filter a list of device monitoring data based on their organization, +you can use the ``organization_id``. .. code-block:: text - GET /api/v1/monitoring/device/?organization={organization_id} + GET /api/v1/monitoring/device/?organization={organization_id} -To filter a list of device monitoring data based -on their organization slug, you can use the ``organization_slug``. +To filter a list of device monitoring data based on their organization +slug, you can use the ``organization_slug``. .. code-block:: text - GET /api/v1/monitoring/device/?organization_slug={organization_slug} + GET /api/v1/monitoring/device/?organization_slug={organization_slug} Collect device metrics and status -################################# +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/monitoring/device/{pk}/?key={key}&time={datetime} -If data is latest then an additional parameter current can also be passed. For e.g.: +If data is latest then an additional parameter current can also be passed. +For e.g.: .. code-block:: text POST /api/v1/monitoring/device/{pk}/?key={key}&time={datetime}¤t=true -The format used for Device Status is inspired by -`NetJSON DeviceMonitoring `_. +The format used for Device Status is inspired by `NetJSON DeviceMonitoring +`_. + +.. note:: -**Note**: the device data will be saved in the timeseries database using -the date time specified ``time``, this should be in the format -``%d-%m-%Y_%H:%M:%S.%f``, otherwise 400 Bad Response will be returned. + The device data will be saved in the timeseries database using the + date time specified ``time``, this should be in the format + ``%d-%m-%Y_%H:%M:%S.%f``, otherwise 400 Bad Response will be returned. -If the request is made without passing the ``time`` argument, -the server local time will be used. +If the request is made without passing the ``time`` argument, the server +local time will be used. -The ``time`` parameter was added to support `resilient collection -and sending of data by the OpenWISP Monitoring Agent +The ``time`` parameter was added to support `resilient collection and +sending of data by the OpenWISP Monitoring Agent `_, this feature allows sending data collected while the device is offline. List nearby devices -################### +~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/monitoring/device/{pk}/nearby-devices/ -Returns list of nearby devices along with respective distance (in metres) and -monitoring status. +Returns list of nearby devices along with respective distance (in metres) +and monitoring status. **Available filters** The list of nearby devices provides the following filters: - ``organization`` (Organization ID of the device) -- ``organization__slug`` (Organization slug of the device) -- ``monitoring__status`` (Monitoring status (``unknown``, ``ok``, ``problem``, or ``critical``)) +- ``organization__slug`` (Organization slug of the device) +- ``monitoring__status`` (Monitoring status (``unknown``, ``ok``, + ``problem``, or ``critical``)) - ``model`` (Pipe `|` separated list of device models) - ``distance__lte`` (Distance in metres) @@ -198,26 +207,26 @@ Here's a few examples: .. code-block:: text - GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization={organization_id} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization__slug={organization_slug} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?monitoring__status={monitoring_status} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2} - GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization={organization_id} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?organization__slug={organization_slug} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?monitoring__status={monitoring_status} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2} + GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance} List wifi session -################# +~~~~~~~~~~~~~~~~~ .. code-block:: text - GET /api/v1/monitoring/wifi-session/ + GET /api/v1/monitoring/wifi-session/ **Available filters** The list of wifi session provides the following filters: - ``device__organization`` (Organization ID of the device) -- ``device`` (Device ID) -- ``device__group`` (Device group ID) +- ``device`` (Device ID) +- ``device__group`` (Device group ID) - ``start_time`` (Start time of the wifi session) - ``stop_time`` (Stop time of the wifi session) @@ -225,38 +234,40 @@ Here's a few examples: .. code-block:: text - GET /api/v1/monitoring/wifi-session/?device__organization={organization_id} - GET /api/v1/monitoring/wifi-session/?device={device_id} - GET /api/v1/monitoring/wifi-session/?device__group={group_id} - GET /api/v1/monitoring/wifi-session/?start_time={stop_time} - GET /api/v1/monitoring/wifi-session/?stop_time={stop_time} + GET /api/v1/monitoring/wifi-session/?device__organization={organization_id} + GET /api/v1/monitoring/wifi-session/?device={device_id} + GET /api/v1/monitoring/wifi-session/?device__group={group_id} + GET /api/v1/monitoring/wifi-session/?start_time={stop_time} + GET /api/v1/monitoring/wifi-session/?stop_time={stop_time} + +.. note:: -**Note:** Both `start_time` and `stop_time` support -greater than or equal to, as well as less than or equal to, filter lookups. + Both `start_time` and `stop_time` support greater than or equal to, as + well as less than or equal to, filter lookups. For example: .. code-block:: text - GET /api/v1/monitoring/wifi-session/?start_time__gt={start_time} - GET /api/v1/monitoring/wifi-session/?start_time__gte={start_time} - GET /api/v1/monitoring/wifi-session/?stop_time__lt={stop_time} - GET /api/v1/monitoring/wifi-session/?stop_time__lte={stop_time} + GET /api/v1/monitoring/wifi-session/?start_time__gt={start_time} + GET /api/v1/monitoring/wifi-session/?start_time__gte={start_time} + GET /api/v1/monitoring/wifi-session/?stop_time__lt={stop_time} + GET /api/v1/monitoring/wifi-session/?stop_time__lte={stop_time} Get wifi session -################ +~~~~~~~~~~~~~~~~ .. code-block:: text - GET /api/v1/monitoring/wifi-session/{id}/ + GET /api/v1/monitoring/wifi-session/{id}/ Pagination -########## +~~~~~~~~~~ -Wifi session endpoint support the ``page_size`` parameter -that allows paginating the results in conjunction with the page parameter. +Wifi session endpoint support the ``page_size`` parameter that allows +paginating the results in conjunction with the page parameter. .. code-block:: text - GET /api/v1/monitoring/wifi-session/?page_size=10 - GET /api/v1/monitoring/wifi-session/?page_size=10&page=1 + GET /api/v1/monitoring/wifi-session/?page_size=10 + GET /api/v1/monitoring/wifi-session/?page_size=10&page=1 diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 7f2d2387..c2fa11c8 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -1,415 +1,420 @@ Settings --------- +======== ``TIMESERIES_DATABASE`` -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- -+--------------+-----------+ -| **type**: | ``str`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ +============ ========= +**type**: ``str`` +**default**: see below +============ ========= .. code-block:: python TIMESERIES_DATABASE = { - 'BACKEND': 'openwisp_monitoring.db.backends.influxdb', - 'USER': 'openwisp', - 'PASSWORD': 'openwisp', - 'NAME': 'openwisp2', - 'HOST': 'localhost', - 'PORT': '8086', - 'OPTIONS': { - 'udp_writes': False, - 'udp_port': 8089, - } + "BACKEND": "openwisp_monitoring.db.backends.influxdb", + "USER": "openwisp", + "PASSWORD": "openwisp", + "NAME": "openwisp2", + "HOST": "localhost", + "PORT": "8086", + "OPTIONS": { + "udp_writes": False, + "udp_port": 8089, + }, } -The following table describes all keys available in ``TIMESERIES_DATABASE`` -setting: - -+---------------+--------------------------------------------------------------------------------------+ -| **Key** | ``Description`` | -+---------------+--------------------------------------------------------------------------------------+ -| ``BACKEND`` | The timeseries database backend to use. You can select one of the backends | -| | located in ``openwisp_monitoring.db.backends`` | -+---------------+--------------------------------------------------------------------------------------+ -| ``USER`` | User for logging into the timeseries database | -+---------------+--------------------------------------------------------------------------------------+ -| ``PASSWORD`` | Password of the timeseries database user | -+---------------+--------------------------------------------------------------------------------------+ -| ``NAME`` | Name of the timeseries database | -+---------------+--------------------------------------------------------------------------------------+ -| ``HOST`` | IP address/hostname of machine where the timeseries database is running | -+---------------+--------------------------------------------------------------------------------------+ -| ``PORT`` | Port for connecting to the timeseries database | -+---------------+--------------------------------------------------------------------------------------+ -| ``OPTIONS`` | These settings depends on the timeseries backend: | -| | | -| | +-----------------+----------------------------------------------------------------+ | -| | | ``udp_writes`` | Whether to use UDP for writing data to the timeseries database | | -| | +-----------------+----------------------------------------------------------------+ | -| | | ``udp_port`` | Timeseries database port for writing data using UDP | | -| | +-----------------+----------------------------------------------------------------+ | -+---------------+--------------------------------------------------------------------------------------+ - -**Note:** UDP packets can have a maximum size of 64KB. When using UDP for writing timeseries -data, if the size of the data exceeds 64KB, TCP mode will be used instead. - -**Note:** If you want to use the ``openwisp_monitoring.db.backends.influxdb`` backend -with UDP writes enabled, then you need to enable two different ports for UDP -(each for different retention policy) in your InfluxDB configuration. The UDP configuration -section of your InfluxDB should look similar to the following: - -.. code-block:: text - - # For writing data with the "default" retention policy - [[udp]] - enabled = true - bind-address = "127.0.0.1:8089" - database = "openwisp2" - - # For writing data with the "short" retention policy - [[udp]] - enabled = true - bind-address = "127.0.0.1:8090" - database = "openwisp2" - retention-policy = 'short' - -If you are using `ansible-openwisp2 `_ -for deploying OpenWISP, you can set the ``influxdb_udp_mode`` ansible variable to ``true`` -in your playbook, this will make the ansible role automatically configure the InfluxDB UDP listeners. -You can refer to the `ansible-ow-influxdb's `_ -(a dependency of ansible-openwisp2) documentation to learn more. +The following table describes all keys available in +``TIMESERIES_DATABASE`` setting: + +============ ============================================================= +**Key** ``Description`` +``BACKEND`` The timeseries database backend to use. You can select one of + the backends located in ``openwisp_monitoring.db.backends`` +``USER`` User for logging into the timeseries database +``PASSWORD`` Password of the timeseries database user +``NAME`` Name of the timeseries database +``HOST`` IP address/hostname of machine where the timeseries database + is running +``PORT`` Port for connecting to the timeseries database +``OPTIONS`` These settings depends on the timeseries backend. Refer the + :ref:`timeseries_backend_options` table below for available + options +============ ============================================================= + +.. _timeseries_backend_options: + +Timeseries database options +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +============== ===================================================== +``udp_writes`` Whether to use UDP for writing data to the timeseries + database +``udp_port`` Timeseries database port for writing data using UDP +============== ===================================================== + +.. important:: + + UDP packets can have a maximum size of 64KB. When using UDP for + writing timeseries data, if the size of the data exceeds 64KB, TCP + mode will be used instead. + +.. note:: + + If you want to use the ``openwisp_monitoring.db.backends.influxdb`` + backend with UDP writes enabled, then you need to enable two different + ports for UDP (each for different retention policy) in your InfluxDB + configuration. The UDP configuration section of your InfluxDB should + look similar to the following: + + .. code-block:: text + + # For writing data with the "default" retention policy + [[udp]] + enabled = true + bind-address = "127.0.0.1:8089" + database = "openwisp2" + + # For writing data with the "short" retention policy + [[udp]] + enabled = true + bind-address = "127.0.0.1:8090" + database = "openwisp2" + retention-policy = 'short' + +If you are using `ansible-openwisp2 +`_ for deploying OpenWISP, +you can set the ``influxdb_udp_mode`` ansible variable to ``true`` in your +playbook, this will make the ansible role automatically configure the +InfluxDB UDP listeners. You can refer to the `ansible-ow-influxdb's +`_ (a +dependency of ansible-openwisp2) documentation to learn more. ``OPENWISP_MONITORING_DEFAULT_RETENTION_POLICY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ -+--------------+--------------------------+ -| **type**: | ``str`` | -+--------------+--------------------------+ -| **default**: | ``26280h0m0s`` (3 years) | -+--------------+--------------------------+ +============ ======================== +**type**: ``str`` +**default**: ``26280h0m0s`` (3 years) +============ ======================== The default retention policy that applies to the timeseries data. ``OPENWISP_MONITORING_SHORT_RETENTION_POLICY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------- -+--------------+-------------+ -| **type**: | ``str`` | -+--------------+-------------+ -| **default**: | ``24h0m0s`` | -+--------------+-------------+ +============ =========== +**type**: ``str`` +**default**: ``24h0m0s`` +============ =========== The default retention policy used to store raw device data. -This data is only used to assess the recent status of devices, keeping -it for a long time would not add much benefit and would cost a lot more -in terms of disk space. +This data is only used to assess the recent status of devices, keeping it +for a long time would not add much benefit and would cost a lot more in +terms of disk space. .. _openwisp_monitoring_auto_ping: ``OPENWISP_MONITORING_AUTO_PING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== Whether ping checks are created automatically for devices. .. _openwisp_monitoring_ping_check_config: ``OPENWISP_MONITORING_PING_CHECK_CONFIG`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------- -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ +============ ======== +**type**: ``dict`` +**default**: ``{}`` +============ ======== -This setting allows to override the default ping check configuration defined in +This setting allows to override the default ping check configuration +defined in ``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``. -For example, if you want to change only the **timeout** of -``ping`` you can use: +For example, if you want to change only the **timeout** of ``ping`` you +can use: .. code-block:: python OPENWISP_MONITORING_PING_CHECK_CONFIG = { - 'timeout': { - 'default': 1000, + "timeout": { + "default": 1000, }, } -If you are overriding the default value for any parameter -beyond the maximum or minimum value defined in -``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``, -you will also need to override the ``maximum`` or ``minimum`` fields -as following: +If you are overriding the default value for any parameter beyond the +maximum or minimum value defined in +``openwisp_monitoring.check.classes.ping.DEFAULT_PING_CHECK_CONFIG``, you +will also need to override the ``maximum`` or ``minimum`` fields as +following: .. code-block:: python OPENWISP_MONITORING_PING_CHECK_CONFIG = { - 'timeout': { - 'default': 2000, - 'minimum': 1500, - 'maximum': 2500, + "timeout": { + "default": 2000, + "minimum": 1500, + "maximum": 2500, }, } -**Note:** Above ``maximum`` and ``minimum`` values are only used for -validating custom parameters of a ``Check`` object. +.. note:: + + Above ``maximum`` and ``minimum`` values are only used for validating + custom parameters of a ``Check`` object. .. _openwisp_monitoring_auto_device_config_check: ``OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -This setting allows you to choose whether `config_applied <#configuration-applied>`_ checks should be -created automatically for newly registered devices. It's enabled by default. +This setting allows you to choose whether `config_applied +<#configuration-applied>`_ checks should be created automatically for +newly registered devices. It's enabled by default. ``OPENWISP_MONITORING_CONFIG_CHECK_INTERVAL`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------- -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``5`` | -+--------------+-------------+ +============ ======= +**type**: ``int`` +**default**: ``5`` +============ ======= This setting allows you to configure the config check interval used by -`config_applied <#configuration-applied>`_. By default it is set to 5 minutes. +`config_applied <#configuration-applied>`_. By default it is set to 5 +minutes. .. _openwisp_monitoring_auto_iperf3: ``OPENWISP_MONITORING_AUTO_IPERF3`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``False`` | -+--------------+-------------+ +============ ========= +**type**: ``bool`` +**default**: ``False`` +============ ========= -This setting allows you to choose whether :ref:`iperf3 ` checks should be -created automatically for newly registered devices. It's disabled by default. +This setting allows you to choose whether :ref:`iperf3 ` checks +should be created automatically for newly registered devices. It's +disabled by default. .. _openwisp_monitoring_iperf3_check_config: ``OPENWISP_MONITORING_IPERF3_CHECK_CONFIG`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ +============ ======== +**type**: ``dict`` +**default**: ``{}`` +============ ======== -This setting allows to override the default iperf3 check configuration defined in +This setting allows to override the default iperf3 check configuration +defined in ``openwisp_monitoring.check.classes.iperf3.DEFAULT_IPERF3_CHECK_CONFIG``. -For example, you can change the values of `supported iperf3 check parameters <#iperf3-check-parameters>`_. +For example, you can change the values of `supported iperf3 check +parameters <#iperf3-check-parameters>`_. .. code-block:: python - OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { - # 'org_pk' : {'host' : [], 'client_options' : {}} - 'a9734710-db30-46b0-a2fc-01f01046fe4f': { - # Some public iperf3 servers - # https://iperf.fr/iperf-servers.php#public-servers - 'host': ['iperf3.openwisp.io', '2001:db8::1', '192.168.5.2'], - 'client_options': { - 'port': 6209, - # Number of parallel client streams to run - # note that iperf3 is single threaded - # so if you are CPU bound this will not - # yield higher throughput - 'parallel': 5, - # Set the connect_timeout (in milliseconds) for establishing - # the initial control connection to the server, the lower the value - # the faster the down iperf3 server will be detected (ex. 1000 ms (1 sec)) - 'connect_timeout': 1000, - # Window size / socket buffer size - 'window': '300K', - # Only one reverse condition can be chosen, - # reverse or bidirectional - 'reverse': True, - # Only one test end condition can be chosen, - # time, bytes or blockcount - 'blockcount': '1K', - 'udp': {'bitrate': '50M', 'length': '1460K'}, - 'tcp': {'bitrate': '20M', 'length': '256K'}, - }, - } - } + OPENWISP_MONITORING_IPERF3_CHECK_CONFIG = { + # 'org_pk' : {'host' : [], 'client_options' : {}} + "a9734710-db30-46b0-a2fc-01f01046fe4f": { + # Some public iperf3 servers + # https://iperf.fr/iperf-servers.php#public-servers + "host": ["iperf3.openwisp.io", "2001:db8::1", "192.168.5.2"], + "client_options": { + "port": 6209, + # Number of parallel client streams to run + # note that iperf3 is single threaded + # so if you are CPU bound this will not + # yield higher throughput + "parallel": 5, + # Set the connect_timeout (in milliseconds) for establishing + # the initial control connection to the server, the lower the value + # the faster the down iperf3 server will be detected (ex. 1000 ms (1 sec)) + "connect_timeout": 1000, + # Window size / socket buffer size + "window": "300K", + # Only one reverse condition can be chosen, + # reverse or bidirectional + "reverse": True, + # Only one test end condition can be chosen, + # time, bytes or blockcount + "blockcount": "1K", + "udp": {"bitrate": "50M", "length": "1460K"}, + "tcp": {"bitrate": "20M", "length": "256K"}, + }, + } + } ``OPENWISP_MONITORING_IPERF3_CHECK_DELETE_RSA_KEY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------------- -+--------------+-------------------------------+ -| **type**: | ``bool`` | -+--------------+-------------------------------+ -| **default**: | ``True`` | -+--------------+-------------------------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -This setting allows you to set whether -:ref:`iperf3 check RSA public key ` -will be deleted after successful completion of the check or not. +This setting allows you to set whether :ref:`iperf3 check RSA public key +` will be deleted after successful +completion of the check or not. ``OPENWISP_MONITORING_IPERF3_CHECK_LOCK_EXPIRE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ -+--------------+-------------------------------+ -| **type**: | ``int`` | -+--------------+-------------------------------+ -| **default**: | ``600`` | -+--------------+-------------------------------+ +============ ======= +**type**: ``int`` +**default**: ``600`` +============ ======= -This setting allows you to set a cache lock expiration time for the iperf3 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)**. +This setting allows you to set a cache lock expiration time for the iperf3 +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_CHARTS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- -+--------------+-----------------------------------------------------------------+ -| **type**: | ``list`` | -+--------------+-----------------------------------------------------------------+ -| **default**: | ``('traffic', 'wifi_clients', 'uptime', 'packet_loss', 'rtt')`` | -+--------------+-----------------------------------------------------------------+ +============ ====================================================== +**type**: ``list`` +**default**: ``('traffic', 'wifi_clients', 'uptime', 'packet_loss', + 'rtt')`` +============ ====================================================== Automatically created charts. ``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------- -+--------------+-----------------------------------------------------------------+ -| **type**: | ``list`` of ``dict`` objects | -+--------------+-----------------------------------------------------------------+ -| **default**: | ``[{'key': 'ping', 'field_name': 'reachable'}]`` | -+--------------+-----------------------------------------------------------------+ +============ ================================================ +**type**: ``list`` of ``dict`` objects +**default**: ``[{'key': 'ping', 'field_name': 'reachable'}]`` +============ ================================================ Device metrics that are considered critical: when a value crosses the boundary defined in the "threshold value" field -of the alert settings related to one of these metric types, the health status -of the device related to the metric moves into ``CRITICAL``. +of the alert settings related to one of these metric types, the health +status of the device related to the metric moves into ``CRITICAL``. -By default, if devices are not reachable by pings they are flagged as ``CRITICAL``. +By default, if devices are not reachable by pings they are flagged as +``CRITICAL``. ``OPENWISP_MONITORING_HEALTH_STATUS_LABELS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- -+--------------+--------------------------------------------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------------------------------------------------+ -| **default**: | ``{'unknown': 'unknown', 'ok': 'ok', 'problem': 'problem', 'critical': 'critical'}`` | -+--------------+--------------------------------------------------------------------------------------+ +============ ========================================================== +**type**: ``dict`` +**default**: ``{'unknown': 'unknown', 'ok': 'ok', 'problem': 'problem', + 'critical': 'critical'}`` +============ ========================================================== This setting allows to change the health status labels, for example, if we -want to use ``online`` instead of ``ok`` and ``offline`` instead of ``critical``, -you can use the following configuration: +want to use ``online`` instead of ``ok`` and ``offline`` instead of +``critical``, you can use the following configuration: .. code-block:: python OPENWISP_MONITORING_HEALTH_STATUS_LABELS = { - 'ok': 'online', - 'problem': 'problem', - 'critical': 'offline' + "ok": "online", + "problem": "problem", + "critical": "offline", } .. _openwisp_monitoring_wifi_sessions_enabled: ``OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -Setting this to ``False`` will disable `Monitoring Wifi Sessions <#monitoring-wifi-sessions>`_ -feature. +Setting this to ``False`` will disable `Monitoring Wifi Sessions +<#monitoring-wifi-sessions>`_ feature. ``OPENWISP_MONITORING_MANAGEMENT_IP_ONLY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ + +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +By default, only the management IP will be used to perform active checks +to the devices. -By default, only the management IP will be used to perform active checks to -the devices. +If the devices are connecting to your OpenWISP instance using a shared +layer2 network, hence the OpenWSP server can reach the devices using the +``last_ip`` field, you can set this to ``False``. -If the devices are connecting to your OpenWISP instance using a shared layer2 -network, hence the OpenWSP server can reach the devices using the ``last_ip`` -field, you can set this to ``False``. +.. note:: -**Note:** If this setting is not configured, it will fallback to the value of -`OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting -`_. -If ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, -then it will fallback to ``True``. + If this setting is not configured, it will fallback to the value of + `OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting + `_. + If ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, + then it will fallback to ``True``. ``OPENWISP_MONITORING_DEVICE_RECOVERY_DETECTION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -When device recovery detection is enabled, recoveries are discovered as soon as -a device contacts the openwisp system again (eg: to get the configuration checksum -or to send monitoring metrics). +When device recovery detection is enabled, recoveries are discovered as +soon as a device contacts the openwisp system again (eg: to get the +configuration checksum or to send monitoring metrics). This feature is enabled by default. -If you use OpenVPN as the management VPN, you may want to check out a similar -integration built in **openwisp-network-topology**: when the status of an OpenVPN link -changes (detected by monitoring the status information of OpenVPN), the -network topology module will trigger the monitoring checks. -For more information see: -`Network Topology Device Integration `_ +If you use OpenVPN as the management VPN, you may want to check out a +similar integration built in **openwisp-network-topology**: when the +status of an OpenVPN link changes (detected by monitoring the status +information of OpenVPN), the network topology module will trigger the +monitoring checks. For more information see: `Network Topology Device +Integration +`_ ``OPENWISP_MONITORING_MAC_VENDOR_DETECTION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== Indicates whether mac addresses will be complemented with hardware vendor -information by performing lookups on the OUI -(Organization Unique Identifier) table. +information by performing lookups on the OUI (Organization Unique +Identifier) table. This feature is enabled by default. ``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+-----------+ -| **type**: | ``dict`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ +============ ========= +**type**: ``dict`` +**default**: see below +============ ========= .. code-block:: python @@ -424,153 +429,162 @@ This feature is enabled by default. Retry settings for recoverable failures during metric writes. -By default if a metric write fails (eg: due to excessive load on timeseries database at that moment) -then the operation will be retried indefinitely with an exponential random backoff and a maximum delay of 10 minutes. +By default if a metric write fails (eg: due to excessive load on +timeseries database at that moment) then the operation will be retried +indefinitely with an exponential random backoff and a maximum delay of 10 +minutes. -This feature makes the monitoring system resilient to temporary outages and helps to prevent data loss. +This feature makes the monitoring system resilient to temporary outages +and helps to prevent data loss. -For more information regarding these settings, consult the `celery documentation -regarding automatic retries for known errors +For more information regarding these settings, consult the `celery +documentation regarding automatic retries for known errors `_. -**Note:** The retry mechanism does not work when using ``UDP`` for writing -data to the timeseries database. It is due to the nature of ``UDP`` protocol -which does not acknowledge receipt of data packets. +.. note:: + + The retry mechanism does not work when using ``UDP`` for writing data + to the timeseries database. It is due to the nature of ``UDP`` + protocol which does not acknowledge receipt of data packets. ``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ -+--------------+-----------+ -| **type**: | ``dict`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ +============ ========= +**type**: ``dict`` +**default**: see below +============ ========= .. code-block:: python # default value of OPENWISP_MONITORING_RETRY_OPTIONS: - dict( - max_retries=6, - delay=2 - ) + dict(max_retries=6, delay=2) -On busy systems, communication with the timeseries DB can occasionally fail. -The timeseries DB backend will retry on any exception according to these settings. -The delay kicks in only after the third consecutive attempt. +On busy systems, communication with the timeseries DB can occasionally +fail. The timeseries DB backend will retry on any exception according to +these settings. The delay kicks in only after the third consecutive +attempt. -This setting shall not be confused with ``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS``, -which is used to configure the infinite retrying of the celery task which writes -metric data to the timeseries DB, while ``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` -deals with any other read/write operation on the timeseries DB which may fail. +This setting shall not be confused with +``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS``, which is used to configure +the infinite retrying of the celery task which writes metric data to the +timeseries DB, while ``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` +deals with any other read/write operation on the timeseries DB which may +fail. -However these retries are not handled by celery but are simple python loops, -which will eventually give up if a problem persists. +However these retries are not handled by celery but are simple python +loops, which will eventually give up if a problem persists. ``OPENWISP_MONITORING_TIMESERIES_RETRY_DELAY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------- -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``2`` | -+--------------+-------------+ +============ ======= +**type**: ``int`` +**default**: ``2`` +============ ======= -This settings allow you to configure the retry delay time (in seconds) after 3 failed attempt in timeseries database. +This settings allow you to configure the retry delay time (in seconds) +after 3 failed attempt in timeseries database. -This retry setting is used in retry mechanism to make the requests to the timeseries database resilient. +This retry setting is used in retry mechanism to make the requests to the +timeseries database resilient. This setting is independent of celery retry settings. ``OPENWISP_MONITORING_DASHBOARD_MAP`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -Whether the geographic map in the dashboard is enabled or not. -This feature provides a geographic map which shows the locations -which have devices installed in and provides a visual representation -of the monitoring status of the devices, this allows to get -an overview of the network at glance. +Whether the geographic map in the dashboard is enabled or not. This +feature provides a geographic map which shows the locations which have +devices installed in and provides a visual representation of the +monitoring status of the devices, this allows to get an overview of the +network at glance. This feature is enabled by default and depends on the setting -``OPENWISP_ADMIN_DASHBOARD_ENABLED`` from -`openwisp-utils `__ -being set to ``True`` (which is the default). +``OPENWISP_ADMIN_DASHBOARD_ENABLED`` from `openwisp-utils +`__ being set to ``True`` +(which is the default). -You can turn this off if you do not use the geographic features -of OpenWISP. +You can turn this off if you do not use the geographic features of +OpenWISP. .. _openwisp_monitoring_dashboard_traffic_chart: ``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------- -+--------------+--------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------+ -| **default**: | ``{'__all__': ['wan', 'eth1', 'eth0.2']}`` | -+--------------+--------------------------------------------+ +============ ========================================== +**type**: ``dict`` +**default**: ``{'__all__': ['wan', 'eth1', 'eth0.2']}`` +============ ========================================== -This settings allows to configure the interfaces which should -be included in the **General Traffic** chart in the admin dashboard. +This settings allows to configure the interfaces which should be included +in the **General Traffic** chart in the admin dashboard. This setting should be defined in the following format: -.. code-block::python +.. + code-block::python OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART = { '': [''] } -E.g., if you want the **General Traffic** chart to show data from -two interfaces for an organization, you need to configure this setting -as follows: +E.g., if you want the **General Traffic** chart to show data from two +interfaces for an organization, you need to configure this setting as +follows: -.. code-block::python +.. + code-block::python OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART = { # organization uuid 'f9601bbd-b6d5-4704-85e3-5851894437bf': ['eth1', 'eth2'] } -**Note**: The value of ``__all__`` key is used if an organization -does not have list of interfaces defined in ``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART``. +.. note:: + + The value of ``__all__`` key is used if an organization does not have + list of interfaces defined in + ``OPENWISP_MONITORING_DASHBOARD_TRAFFIC_CHART``. -**Note**: If a user can manage more than one organization (e.g. superusers), -then the **General Traffic** chart will always show data from interfaces -of ``__all__`` configuration. +.. note:: + + If a user can manage more than one organization (e.g. superusers), + then the **General Traffic** chart will always show data from + interfaces of ``__all__`` configuration. .. _openwisp_monitoring_metrics: ``OPENWISP_MONITORING_METRICS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ +============ ======== +**type**: ``dict`` +**default**: ``{}`` +============ ======== -This setting allows to define additional metric configuration or to override -the default metric configuration defined in +This setting allows to define additional metric configuration or to +override the default metric configuration defined in ``openwisp_monitoring.monitoring.configuration.DEFAULT_METRICS``. -For example, if you want to change only the **field_name** of -``clients`` metric to ``wifi_clients`` you can use: +For example, if you want to change only the **field_name** of ``clients`` +metric to ``wifi_clients`` you can use: .. code-block:: python from django.utils.translation import gettext_lazy as _ OPENWISP_MONITORING_METRICS = { - 'clients': { - 'label': _('WiFi clients'), - 'field_name': 'wifi_clients', + "clients": { + "label": _("WiFi clients"), + "field_name": "wifi_clients", }, } @@ -580,9 +594,7 @@ For example, if you want to change only the default alert settings of .. code-block:: python OPENWISP_MONITORING_METRICS = { - 'memory': { - 'alert_settings': {'threshold': 75, 'tolerance': 10} - }, + "memory": {"alert_settings": {"threshold": 75, "tolerance": 10}}, } For example, if you want to change only the notification of @@ -593,30 +605,30 @@ For example, if you want to change only the notification of from django.utils.translation import gettext_lazy as _ OPENWISP_MONITORING_METRICS = { - 'config_applied': { - 'notification': { - 'problem': { - 'verbose_name': 'Configuration PROBLEM', - 'verb': _('has not been applied'), - 'email_subject': _( - '[{site.name}] PROBLEM: {notification.target} configuration ' - 'status issue' + "config_applied": { + "notification": { + "problem": { + "verbose_name": "Configuration PROBLEM", + "verb": _("has not been applied"), + "email_subject": _( + "[{site.name}] PROBLEM: {notification.target} configuration " + "status issue" ), - 'message': _( - 'The configuration for device [{notification.target}]' - '({notification.target_link}) {notification.verb} in a timely manner.' + "message": _( + "The configuration for device [{notification.target}]" + "({notification.target_link}) {notification.verb} in a timely manner." ), }, - 'recovery': { - 'verbose_name': 'Configuration RECOVERY', - 'verb': _('configuration has been applied again'), - 'email_subject': _( - '[{site.name}] RECOVERY: {notification.target} {notification.verb} ' - 'successfully' + "recovery": { + "verbose_name": "Configuration RECOVERY", + "verb": _("configuration has been applied again"), + "email_subject": _( + "[{site.name}] RECOVERY: {notification.target} {notification.verb} " + "successfully" ), - 'message': _( - 'The device [{notification.target}]({notification.target_link}) ' - '{notification.verb} successfully.' + "message": _( + "The device [{notification.target}]({notification.target_link}) " + "{notification.verb} successfully." ), }, }, @@ -624,35 +636,35 @@ For example, if you want to change only the notification of } Or if you want to define a new metric configuration, which you can then -call in your custom code (eg: a custom check class), you can do so as follows: +call in your custom code (eg: a custom check class), you can do so as +follows: .. code-block:: python from django.utils.translation import gettext_lazy as _ OPENWISP_MONITORING_METRICS = { - 'top_fields_mean': { - 'name': 'Top Fields Mean', - 'key': '{key}', - 'field_name': '{field_name}', - 'label': '_(Top fields mean)', - 'related_fields': ['field1', 'field2', 'field3'], + "top_fields_mean": { + "name": "Top Fields Mean", + "key": "{key}", + "field_name": "{field_name}", + "label": "_(Top fields mean)", + "related_fields": ["field1", "field2", "field3"], }, } .. _openwisp_monitoring_charts: ``OPENWISP_MONITORING_CHARTS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------ -+--------------+-------------+ -| **type**: | ``dict`` | -+--------------+-------------+ -| **default**: | ``{}`` | -+--------------+-------------+ +============ ======== +**type**: ``dict`` +**default**: ``{}`` +============ ======== -This setting allows to define additional charts or to override -the default chart configuration defined in +This setting allows to define additional charts or to override the default +chart configuration defined in ``openwisp_monitoring.monitoring.configuration.DEFAULT_CHARTS``. In the following example, we modify the description of the traffic chart: @@ -660,30 +672,31 @@ In the following example, we modify the description of the traffic chart: .. code-block:: python OPENWISP_MONITORING_CHARTS = { - 'traffic': { - 'description': ( - 'Network traffic, download and upload, measured on ' + "traffic": { + "description": ( + "Network traffic, download and upload, measured on " 'the interface "{metric.key}", custom message here.' ), } } Or if you want to define a new chart configuration, which you can then -call in your custom code (eg: a custom check class), you can do so as follows: +call in your custom code (eg: a custom check class), you can do so as +follows: .. code-block:: python from django.utils.translation import gettext_lazy as _ OPENWISP_MONITORING_CHARTS = { - 'ram': { - 'type': 'line', - 'title': 'RAM usage', - 'description': 'RAM usage', - 'unit': 'bytes', - 'order': 100, - 'query': { - 'influxdb': ( + "ram": { + "type": "line", + "title": "RAM usage", + "description": "RAM usage", + "unit": "bytes", + "order": 100, + "query": { + "influxdb": ( "SELECT MEAN(total) AS total, MEAN(free) AS free, " "MEAN(buffered) AS buffered FROM {key} WHERE time >= '{time}' AND " "content_type = '{content_type}' AND object_id = '{object_id}' " @@ -693,75 +706,69 @@ call in your custom code (eg: a custom check class), you can do so as follows: } } -In case you just want to change the colors used in a chart here's how to do it: +In case you just want to change the colors used in a chart here's how to +do it: .. code-block:: python OPENWISP_MONITORING_CHARTS = { - 'traffic': { - 'colors': ['#000000', '#cccccc', '#111111'] - } + "traffic": {"colors": ["#000000", "#cccccc", "#111111"]} } ``OPENWISP_MONITORING_DEFAULT_CHART_TIME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ -+---------------------+---------------------------------------------+ -| **type**: | ``str`` | -+---------------------+---------------------------------------------+ -| **default**: | ``7d`` | -+---------------------+---------------------------------------------+ -| **possible values** | ``1d``, ``3d``, ``7d``, ``30d`` or ``365d`` | -+---------------------+---------------------------------------------+ +=================== =========================================== +**type**: ``str`` +**default**: ``7d`` +**possible values** ``1d``, ``3d``, ``7d``, ``30d`` or ``365d`` +=================== =========================================== Allows to set the default time period of the time series charts. ``OPENWISP_MONITORING_AUTO_CLEAR_MANAGEMENT_IP`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== This setting allows you to automatically clear management_ip of a device when it goes offline. It is enabled by default. ``OPENWISP_MONITORING_API_URLCONF`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- -+--------------+-------------+ -| **type**: | ``string`` | -+--------------+-------------+ -| **default**: | ``None`` | -+--------------+-------------+ +============ ========== +**type**: ``string`` +**default**: ``None`` +============ ========== -Changes the urlconf option of django urls to point the monitoring API -urls to another installed module, example, ``myapp.urls``. -(Useful when you have a seperate API instance.) +Changes the urlconf option of django urls to point the monitoring API urls +to another installed module, example, ``myapp.urls``. (Useful when you +have a seperate API instance.) ``OPENWISP_MONITORING_API_BASEURL`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- -+--------------+-------------+ -| **type**: | ``string`` | -+--------------+-------------+ -| **default**: | ``None`` | -+--------------+-------------+ +============ ========== +**type**: ``string`` +**default**: ``None`` +============ ========== -If you have a seperate server for API of openwisp-monitoring on a different -domain, you can use this option to change the base of the url, this will -enable you to point all the API urls to your openwisp-monitoring API server's -domain, example: ``https://mymonitoring.myapp.com``. +If you have a seperate server for API of openwisp-monitoring on a +different domain, you can use this option to change the base of the url, +this will enable you to point all the API urls to your openwisp-monitoring +API server's domain, example: ``https://mymonitoring.myapp.com``. ``OPENWISP_MONITORING_CACHE_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------- -+--------------+----------------------------------+ -| **type**: | ``int`` | -+--------------+----------------------------------+ -| **default**: | ``86400`` (24 hours in seconds) | -+--------------+----------------------------------+ +============ =============================== +**type**: ``int`` +**default**: ``86400`` (24 hours in seconds) +============ =============================== -This setting allows to configure timeout (in seconds) for monitoring data cache. +This setting allows to configure timeout (in seconds) for monitoring data +cache. diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 3104dcd9..0c2647a0 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -1,5 +1,5 @@ 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 @@ -10,44 +10,49 @@ You can filter both currently open sessions and past sessions by their 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 + :align: center .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-session-change.png - :align: center + :align: center You can disable this feature by configuring -:ref:`OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED ` -setting. +:ref:`OPENWISP_MONITORING_WIFI_SESSIONS_ENABLED +` setting. -You can also view open WiFi sessions of a device directly from the device's change admin -under the "WiFi Sessions" tab. +You can also view open WiFi sessions of a device directly from the +device's change admin under the "WiFi Sessions" tab. .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-wifi-session-inline.png - :align: center + :align: center 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 :ref:`setup instructions `. +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 :ref:`setup instructions +`. -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. +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: +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 + "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 `_ +Please refer to `"Periodic Tasks" section of Celery's documentation +`_ to learn more. From 4d37fc9f321e332401ddb6e75e0cf8c5a8a3f121 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 29 May 2024 00:12:36 +0530 Subject: [PATCH 09/42] [docs] Restructured docs and fixed URLs --- docs/developer/developer-docs.rst | 17 --- docs/developer/index.rst | 16 +++ docs/developer/management-commands.rst | 6 +- ...ring-unregistering-chart-configuration.rst | 31 +++++ docs/index.rst | 106 +++-------------- docs/partials/developer-docs.rst | 12 ++ docs/user/adding-checks-and-alertsettings.rst | 24 ++-- .../user/{available-checks.rst => checks.rst} | 39 ++++-- ...tions.rst => configuring-iperf3-check.rst} | 29 ++--- docs/user/dashboard-monitoring-charts.rst | 3 +- .../user/default-alerts-and-notifications.rst | 17 --- docs/user/intro.rst | 49 ++++++++ .../user/{default-metrics.rst => metrics.rst} | 111 ++++++++++++++---- .../passive-vs-active-metric-collection.rst | 18 --- docs/user/quickstart.rst | 71 +++-------- docs/user/rest-api.rst | 77 ++++++------ docs/user/settings.rst | 77 +++++++++--- docs/user/wifi-sessions.rst | 18 ++- 18 files changed, 407 insertions(+), 314 deletions(-) delete mode 100644 docs/developer/developer-docs.rst create mode 100644 docs/developer/index.rst create mode 100644 docs/partials/developer-docs.rst rename docs/user/{available-checks.rst => checks.rst} (65%) rename docs/user/{iperf3-usage-instructions.rst => configuring-iperf3-check.rst} (91%) delete mode 100644 docs/user/default-alerts-and-notifications.rst create mode 100644 docs/user/intro.rst rename docs/user/{default-metrics.rst => metrics.rst} (53%) delete mode 100644 docs/user/passive-vs-active-metric-collection.rst diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst deleted file mode 100644 index de514fe9..00000000 --- a/docs/developer/developer-docs.rst +++ /dev/null @@ -1,17 +0,0 @@ -Developers Documentation -======================== - -.. include:: /partials/developers-docs-warning.rst - -.. toctree:: - :maxdepth: 1 - - ./installation.rst - ./management-commands.rst - ./monitoring-scripts.rst - ./registering-unregistering-metric-configuration.rst - ./registering-unregistering-chart-configuration.rst - ./registering-new-notification-types.rst - ./exceptions.rst - ./signals.rst - ./extending.rst diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 00000000..20098027 --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,16 @@ +Developers Documentation +======================== + +.. include:: ../partials/developer-docs.rst + +.. toctree:: + :maxdepth: 2 + + ./installation.rst + ./utils.rst + ./extending.rst + +Other useful resources: + + - :doc:`../user/rest-api` + - :doc:`../user/settings` diff --git a/docs/developer/management-commands.rst b/docs/developer/management-commands.rst index 4f173c59..89b5b98b 100644 --- a/docs/developer/management-commands.rst +++ b/docs/developer/management-commands.rst @@ -8,9 +8,9 @@ Management commands ``run_checks`` -------------- -This command will execute all the `available checks `_ -for all the devices. By default checks are run periodically by *celery -beat*. You can learn more about this in :ref:`Setup +This command will execute all the `available checks `_ for all the +devices. By default checks are run periodically by *celery beat*. You can +learn more about this in :ref:`Setup `. Example usage: diff --git a/docs/developer/registering-unregistering-chart-configuration.rst b/docs/developer/registering-unregistering-chart-configuration.rst index fd5085c1..897af7ba 100644 --- a/docs/developer/registering-unregistering-chart-configuration.rst +++ b/docs/developer/registering-unregistering-chart-configuration.rst @@ -58,6 +58,37 @@ If you don't need to register a new chart but need to change a specific key of an existing chart configuration, you can use :ref:`OPENWISP_MONITORING_CHARTS `. +Adaptive size charts +~~~~~~~~~~~~~~~~~~~~ + +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/adaptive-chart.png + :align: center + +When configuring charts, it is possible to flag their unit as +``adaptive_prefix``, this allows to make the charts more readable because +the units are shown in either `K`, `M`, `G` and `T` depending on the size +of each point, the summary values and Y axis are also resized. + +Example taken from the default configuration of the traffic chart: + +.. code-block:: python + + OPENWISP_MONITORING_CHARTS = { + "traffic": { + # other configurations for this chart + # traffic measured in 'B' (bytes) + # unit B, KB, MB, GB, TB + "unit": "adaptive_prefix+B", + }, + "bandwidth": { + # other configurations for this chart + # adaptive unit for bandwidth related charts + # bandwidth measured in 'bps'(bits/sec) + # unit bps, Kbps, Mbps, Gbps, Tbps + "unit": "adaptive_prefix+bps", + }, + } + ``unregister_chart`` -------------------- diff --git a/docs/index.rst b/docs/index.rst index 539d3f46..1d1550a3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,106 +1,32 @@ -OpenWISP Monitoring -=================== +Monitoring +========== OpenWISP Monitoring is a network monitoring system written in Python and Django, designed to be **extensible**, **programmable**, **scalable** and easy to use by end users: once the system is configured, monitoring checks, alerts and metric collection happens automatically. -See the `available features <#available-features>`_. - -`OpenWISP `_ is not only an application designed for -end users, but can also be used as a framework on which custom network -automation solutions can be built on top of its building blocks. - -Other popular building blocks that are part of the OpenWISP ecosystem are: - -- `openwisp-controller - `_: network and WiFi - controller: provisioning, configuration management, x509 PKI management - and more; works on OpenWRT, but designed to work also on other systems. -- `openwisp-network-topology - `_: provides way - to collect and visualize network topology data from dynamic mesh routing - daemons or other network software (eg: OpenVPN); it can be used in - conjunction with openwisp-monitoring to get a better idea of the state - of the network -- `openwisp-firmware-upgrader - `_: automated - firmware upgrades (single device or mass network upgrades) -- `openwisp-radius `_: based - on FreeRADIUS, allows to implement network access authentication systems - like 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 - (802.11u) -- `openwisp-ipam `_: it allows - to manage the IP address space of networks - -**For a more complete overview of the OpenWISP modules and architecture**, -see the `OpenWISP Architecture Overview -`_. - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png - :align: center - -**Available Features** - -- Collection of monitoring information in a timeseries database (currently - only influxdb is supported) -- Allows to browse alerts easily from the user interface with one click -- Collects and displays `device status <#device-status>`_ information like - uptime, RAM status, CPU load averages, Interface properties and - addresses, WiFi interface status and associated clients, Neighbors - information, DHCP Leases, Disk/Flash status -- Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, `round - trip time (latency) <#ping>`_, `associated wifi clients - <#wifi-clients>`_, `interface traffic <#traffic>`_, `RAM usage - <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage - <#disk-usage>`_, mobile signal (LTE/UMTS/GSM `signal strength - <#mobile-signal-strength>`_, `signal quality <#mobile-signal-quality>`_, - `access technology in use <#mobile-access-technology-in-use>`_), - `bandwidth <#iperf3>`_, `transferred data <#iperf3>`_, `restransmits - <#iperf3>`_, `jitter <#iperf3>`_, `datagram <#iperf3>`_, `datagram loss - <#iperf3>`_ -- 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 the last 1 day, 3 days, 7 days, - 30 days, and 365 days -- Configurable alerts -- CSV Export of monitoring data -- An overview of the status of the network is shown in the admin - dashboard, a chart shows the percentages of devices which are online, - offline or having issues; there are also `two timeseries charts which - show the total unique WiFI clients and the traffic flowing to the - network `_, a geographic map is also - available for those who use the geographic features of OpenWISP -- Possibility to configure additional :ref:`Metrics - ` and :ref:`Charts - ` -- Extensible active check system: it's possible to write additional checks - that are run periodically using python classes -- Extensible metrics and charts: it's possible to define new metrics and - new charts -- API to retrieve the chart metrics and status information of each device - based on `NetJSON DeviceMonitoring - `_ -- :ref:`Iperf3 check ` that provides network performance - measurements such as maximum achievable bandwidth, jitter, datagram loss - etc of the openwrt device using `iperf3 utility `_ +Refer to :doc:`user/intro` for a complete overview of features. .. toctree:: + :caption: User Docs :maxdepth: 1 + ./user/intro.rst ./user/quickstart.rst - ./user/passive-vs-active-metric-collection.rst ./user/device-health-status.rst - ./user/default-metrics.rst + ./user/metrics.rst + ./user/checks.rst + ./user/adding-checks-and-alertsettings.rst + ./user/configuring-iperf3-check.rst ./user/dashboard-monitoring-charts.rst - ./user/adaptive-size-charts.rst ./user/wifi-sessions.rst ./user/default-alerts-and-notifications.rst - ./user/available-checks.rst - ./user/iperf3-usage-instructions.rst - ./user/adding-checks-and-alertsettings.rst - ./user/settings.rst ./user/rest-api.rst - ./developer/developer-docs.rst + ./user/settings.rst + +.. toctree:: + :caption: Developer Docs + :maxdepth: 2 + + Developer Docs Index diff --git a/docs/partials/developer-docs.rst b/docs/partials/developer-docs.rst new file mode 100644 index 00000000..4047ab4b --- /dev/null +++ b/docs/partials/developer-docs.rst @@ -0,0 +1,12 @@ +.. note:: + + This documentation page is aimed at developers who want to customize, + change or extend the code of OpenWISP Monitoring in order to modify + its behavior (eg: for personal or commercial purposes or to fix a bug, + implement a new feature or contribute to the project in general). + + If you aren't a developer and you are looking for information on how + to use OpenWISP, please refer to: + + - :doc:`General OpenWISP Quickstart ` + - :doc:`OpenWISP Monitoring User Docs ` diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst index aa629715..9517769b 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -9,20 +9,21 @@ page**. To add a check, you just need to select an available **check type** as shown below: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-check.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/device-inline-check.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/device-inline-check.png :align: center The following example shows how to use the -:ref:`OPENWISP_MONITORING_METRICS setting ` -to reconfigure the system for :ref:`iperf3 check ` to send an -alert if the measured **TCP bandwidth** has been less than **10 Mbit/s** -for more than **2 days**. +:ref:`openwisp_monitoring_metrics` setting to reconfigure the system for +:ref:`iperf3 check ` to send an alert if the measured **TCP +bandwidth** has been less than **10Mbit/s** for more than **2 days**. -1. By default, :ref:`Iperf3 checks ` come with default alert +1. By default, :ref:`Iperf3 checks ` come with default alert settings, but it is easy to customize alert settings through the device page as shown below: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/device-inline-alertsettings.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/device-inline-alertsettings.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/device-inline-alertsettings.png :align: center 2. Now, add the following notification configuration to send an alert for @@ -64,10 +65,12 @@ page as shown below: }, } -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_warn.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/alert_field_warn.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/alert_field_warn.png :align: center -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/alert_field_info.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/alert_field_info.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/alert_field_info.png :align: center .. note:: @@ -77,5 +80,6 @@ page as shown below: included by default in the "Administrator" and "Operator" groups and are shown in the screenshot below. -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/inline-permissions.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/inline-permissions.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/inline-permissions.png :align: center diff --git a/docs/user/available-checks.rst b/docs/user/checks.rst similarity index 65% rename from docs/user/available-checks.rst rename to docs/user/checks.rst index fd088ce8..a5af2798 100644 --- a/docs/user/available-checks.rst +++ b/docs/user/checks.rst @@ -1,5 +1,7 @@ -Available Checks -================ +Checks +====== + +.. _ping_check: Ping ---- @@ -8,12 +10,12 @@ This check returns information on device ``uptime`` and ``RTT (Round trip time)``. The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. The ``fping`` command is used to collect these metrics. You may choose to disable auto creation of this check by setting -:ref:`OPENWISP_MONITORING_AUTO_PING ` to -``False``. +:ref:`openwisp_monitoring_auto_ping` to ``False``. You can change the default values used for ping checks using -:ref:`OPENWISP_MONITORING_PING_CHECK_CONFIG -` setting. +:ref:`openwisp_monitoring_ping_check_config` setting. + +.. _config_applied_check: Configuration applied --------------------- @@ -22,15 +24,14 @@ This check ensures that the `openwisp-config agent `_ is running and applying configuration changes in a timely manner. You may choose to disable auto creation of this check by using the setting -:ref:`OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK -`. +:ref:`openwisp_monitoring_auto_device_config_check`. This check runs periodically, but it is also triggered whenever the configuration status of a device changes, this ensures the check reacts quickly to events happening in the network and informs the user promptly if there's anything that is not working as intended. -.. _iperf3-1: +.. _iperf3_check: Iperf3 ------ @@ -40,8 +41,8 @@ achievable bandwidth, jitter, datagram loss etc of the device using `iperf3 utility `_. This check is **disabled by default**. You can enable auto creation of -this check by setting the :ref:`OPENWISP_MONITORING_AUTO_IPERF3 -` to ``True``. +this check by setting the :ref:`openwisp_monitoring_auto_iperf3` to +``True``. You can also :ref:`add the iperf3 check ` directly from the device page. @@ -57,3 +58,19 @@ rsa_publc_key etc) using the may need to update the :ref:`metric configuration ` to enable alerts for the iperf3 check. + +Alerts / Notifications +---------------------- + +The following kind of notifications will be sent based on the check +results: + +- ``threshold_crossed``: Fires when a metric crosses the boundary defined + in the threshold value of the alert settings. +- ``threhold_recovery``: Fires when a metric goes back within the expected + range. +- ``connection_is_working``: Fires when the connection to a device is + working. +- ``connection_is_not_working``: Fires when the connection (eg: SSH) to a + device stops working (eg: credentials are outdated, management IP + address is outdated, or device is not reachable). diff --git a/docs/user/iperf3-usage-instructions.rst b/docs/user/configuring-iperf3-check.rst similarity index 91% rename from docs/user/iperf3-usage-instructions.rst rename to docs/user/configuring-iperf3-check.rst index 2d478879..73d543b9 100644 --- a/docs/user/iperf3-usage-instructions.rst +++ b/docs/user/configuring-iperf3-check.rst @@ -1,5 +1,5 @@ -Iperf3 Check Usage Instructions -=============================== +Configuring Iperf3 Check +======================== 1. Make sure iperf3 is installed on the device ---------------------------------------------- @@ -16,17 +16,17 @@ device, eg: 2. Ensure SSH access from OpenWISP is enabled on your devices ------------------------------------------------------------- -Follow the steps in `"How to configure push updates" section of the -OpenWISP documentation -`_ to allow SSH +Follow the steps in :ref:`"Configuring Push Operations" +` section of the documentation to allow SSH access to you device from OpenWISP. -.. note:: +.. important:: Make sure device connection is enabled & working with right update strategy i.e. ``OpenWRT SSH``. -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/enable-openwrt-ssh.png +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/enable-openwrt-ssh.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/enable-openwrt-ssh.png :alt: Enable ssh access from openwisp to device :align: center @@ -113,9 +113,12 @@ run this check manually using the :ref:`run_checks ` command. After that, you should see the iperf3 network measurements charts. -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/iperf3-charts.png +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/iperf3-charts.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/iperf3-charts.png :alt: Iperf3 network measurement charts +.. _iperf3_check_parameters: + Iperf3 check parameters ----------------------- @@ -203,9 +206,8 @@ Server side After running the commands mentioned above, the public key will be stored in ``public_key.pem`` which will be used in **rsa_public_key** parameter -in :ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -` and the private key will be -contained in the file ``private_key.pem`` which will be used with +in :ref:`openwisp_monitoring_iperf3_check_config` and the private key will +be contained in the file ``private_key.pem`` which will be used with **--rsa-private-key-path** command option when starting the iperf3 server. 2. Create user credentials @@ -253,14 +255,13 @@ You may also check your installed **iperf3 openwrt package** features: Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, sendfile / zerocopy, socket pacing, authentication # contains 'authentication' -.. _configure-iperf3-check-auth-parameters: +.. _configure_iperf3_check_auth_parameters: 2. Configure iperf3 check auth parameters +++++++++++++++++++++++++++++++++++++++++ Now, add the following iperf3 authentication parameters to -:ref:`OPENWISP_MONITORING_IPERF3_CHECK_CONFIG -` in the settings: +:ref:`openwisp_monitoring_iperf3_check_config` in the settings: .. code-block:: python diff --git a/docs/user/dashboard-monitoring-charts.rst b/docs/user/dashboard-monitoring-charts.rst index 866d7ed3..e2a6a86a 100644 --- a/docs/user/dashboard-monitoring-charts.rst +++ b/docs/user/dashboard-monitoring-charts.rst @@ -1,7 +1,8 @@ Dashboard Monitoring Charts =========================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/dashboard-charts.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/dashboard-charts.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/dashboard-charts.png :align: center OpenWISP Monitoring adds two timeseries charts to the admin dashboard: diff --git a/docs/user/default-alerts-and-notifications.rst b/docs/user/default-alerts-and-notifications.rst deleted file mode 100644 index 41090641..00000000 --- a/docs/user/default-alerts-and-notifications.rst +++ /dev/null @@ -1,17 +0,0 @@ -Default Alerts / Notifications -============================== - -============================= ============================================ -Notification Type Use -``threshold_crossed`` Fires when a metric crosses the boundary - defined in the threshold value of the alert - settings. -``threshold_recovery`` Fires when a metric goes back within the - expected range. -``connection_is_working`` Fires when the connection to a device is - working. -``connection_is_not_working`` Fires when the connection (eg: SSH) to a - device stops working (eg: credentials are - outdated, management IP address is outdated, - or device is not reachable). -============================= ============================================ diff --git a/docs/user/intro.rst b/docs/user/intro.rst new file mode 100644 index 00000000..acb8d8f7 --- /dev/null +++ b/docs/user/intro.rst @@ -0,0 +1,49 @@ +Monitoring: Features +==================== + +OpenWISP provides the following monitoring capabilities: + +- Collection of monitoring information in a timeseries database (currently + only influxdb is supported) +- Allows to browse alerts easily from the user interface with one click +- Collects and displays :ref:`device status ` information + like uptime, RAM status, CPU load averages, Interface properties and + addresses, WiFi interface status and associated clients, Neighbors + information, DHCP Leases, Disk/Flash status +- Monitoring charts for :ref:`uptime `, :ref:`packet loss `, + :ref:`round trip time (latency) `, :ref:`associated wifi clients + `, :ref:`interface traffic `, :ref:`RAM usage + `, :ref:`CPU load `, :ref:`flash/disk usage + `, mobile signal (LTE/UMTS/GSM :ref:`signal strength + `, :ref:`signal quality + `, :ref:`access technology in use + `), :ref:`bandwidth `, + :ref:`transferred data `, :ref:`restransmits `, + :ref:`jitter `, :ref:`datagram `, :ref:`datagram loss + ` +- Maintains a record of :doc:`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 the last 1 day, 3 days, 7 days, + 30 days, and 365 days +- Configurable alerts +- CSV Export of monitoring data +- An overview of the status of the network is shown in the admin + dashboard, a chart shows the percentages of devices which are online, + offline or having issues; there are also :doc:`two timeseries charts + which show the total unique WiFI clients and the traffic flowing to the + network `, a geographic map is also + available for those who use the geographic features of OpenWISP +- Possibility to configure additional :ref:`Metrics + ` and :ref:`Charts + ` +- Extensible active check system: it's possible to write additional checks + that are run periodically using python classes +- Extensible metrics and charts: it's possible to define new metrics and + new charts +- API to retrieve the chart metrics and status information of each device + based on `NetJSON DeviceMonitoring + `_ +- :ref:`Iperf3 check ` that provides network performance + measurements such as maximum achievable bandwidth, jitter, datagram loss + etc of the openwrt device using `iperf3 utility `_ diff --git a/docs/user/default-metrics.rst b/docs/user/metrics.rst similarity index 53% rename from docs/user/default-metrics.rst rename to docs/user/metrics.rst index 91611c2d..7d1cf473 100644 --- a/docs/user/default-metrics.rst +++ b/docs/user/metrics.rst @@ -1,23 +1,31 @@ -Default Metrics -=============== +Metrics +======= + +.. _device_status: Device Status ------------- This metric stores the status of the device for viewing purposes. -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-1.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-1.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-1.png :align: center -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-2.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-2.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-2.png :align: center -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-3.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-3.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-3.png :align: center -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-status-4.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-4.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-status-4.png :align: center +.. _ping: + Ping ---- @@ -32,19 +40,24 @@ Ping **Uptime**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/uptime.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/uptime.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/uptime.png :align: center **Packet loss**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/packet-loss.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/packet-loss.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/packet-loss.png :align: center **Round Trip Time**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/rtt.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/rtt.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/rtt.png :align: center +.. _traffic: + Traffic ------- @@ -65,9 +78,12 @@ Traffic **charts**: ``traffic`` ================== ========================================================================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/traffic.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/traffic.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/traffic.png :align: center +.. _wifi_clients: + WiFi Clients ------------ @@ -88,9 +104,12 @@ WiFi Clients **charts**: ``wifi_clients`` ================== ========================================================================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-clients.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/wifi-clients.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/wifi-clients.png :align: center +.. _memory_usage: + Memory Usage ------------ @@ -104,9 +123,12 @@ Memory Usage **charts**: ``memory`` ================== ==================================================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/memory.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/memory.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/memory.png :align: center +.. _cpu_load: + CPU Load -------- @@ -118,9 +140,12 @@ CPU Load **charts**: ``load`` ================== ================================================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/cpu-load.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/cpu-load.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/cpu-load.png :align: center +.. _disk_usage: + Disk Usage ---------- @@ -132,9 +157,12 @@ Disk Usage **charts**: ``disk`` ================== ============= -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/disk-usage.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/disk-usage.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/disk-usage.png :align: center +.. _mobile_signal_strength: + Mobile Signal Strength ---------------------- @@ -146,9 +174,12 @@ Mobile Signal Strength **charts**: ``signal_strength`` ================== ===================================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-strength.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-strength.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-strength.png :align: center +.. _mobile_signal_quality: + Mobile Signal Quality --------------------- @@ -160,9 +191,12 @@ Mobile Signal Quality **charts**: ``signal_quality`` ================== ====================================== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/signal-quality.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-quality.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-quality.png :align: center +.. _mobile_access_technology_in_use: + Mobile Access Technology in use ------------------------------- @@ -174,9 +208,12 @@ Mobile Access Technology in use **charts**: ``access_tech`` ================== =============== -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/access-technology.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/access-technology.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/access-technology.png :align: center +.. _iperf3: + Iperf3 ------ @@ -200,39 +237,63 @@ Iperf3 **Bandwidth**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/bandwidth.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/bandwidth.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/bandwidth.png :align: center **Transferred Data**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/transferred-data.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/transferred-data.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/transferred-data.png :align: center **Retransmits**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/retransmits.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/retransmits.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/retransmits.png :align: center **Jitter**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/jitter.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/jitter.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/jitter.png :align: center **Datagram**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/datagram.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/datagram.png :align: center **Datagram loss**: -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/datagram-loss.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/datagram-loss.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/datagram-loss.png :align: center -For more info on how to configure and use Iperf3, please refer to `iperf3 -check usage instructions <#iperf3-check-usage-instructions>`_. +For more info on how to configure and use Iperf3, please refer to +:doc:`configuring-iperf3-check`. .. note:: Iperf3 charts uses ``connect_points=True`` in :ref:`default chart configuration ` that joins it's individual chart data points. + +Passive vs Active Metric Collection +----------------------------------- + +The the different :doc:`device metric <./metrics>` collected by OpenWISP +Monitoring can be divided in two categories: + +1. **metrics collected actively by OpenWISP**: these metrics are collected + by the celery workers running on the OpenWISP server, which + continuously sends network requests to the devices and store the + results; +2. **metrics collected passively by OpenWISP**: these metrics are sent by + the `openwrt-openwisp-monitoring agent + <#install-monitoring-packages-on-the-device>`_ installed on the network + devices and are collected by OpenWISP via its REST API. + +The :doc:`checks` section of the documentation lists the currently +implemented **active checks**. diff --git a/docs/user/passive-vs-active-metric-collection.rst b/docs/user/passive-vs-active-metric-collection.rst deleted file mode 100644 index ba681d24..00000000 --- a/docs/user/passive-vs-active-metric-collection.rst +++ /dev/null @@ -1,18 +0,0 @@ -Passive vs Active Metric Collection -=================================== - -The `the different device metric -`_ -collected by OpenWISP Monitoring can be divided in two categories: - -1. **metrics collected actively by OpenWISP**: these metrics are collected - by the celery workers running on the OpenWISP server, which - continuously sends network requests to the devices and store the - results; -2. **metrics collected passively by OpenWISP**: these metrics are sent by - the `openwrt-openwisp-monitoring agent - <#install-monitoring-packages-on-the-device>`_ installed on the network - devices and are collected by OpenWISP via its REST API. - -The `"Available Checks" <#available-checks>`_ section of this document -lists the currently implemented **active checks**. diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 9fe4b810..07643079 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,49 +1,28 @@ Quickstart Guide ================ -Install OpenWISP Monitoring ---------------------------- - -Install *OpenWISP Monitoring* using one of the methods mentioned in the -`"Installation instructions" <#installation-instructions>`_. - -Install openwisp-config on the device -------------------------------------- - -`Install the openwisp-config agent for OpenWrt -`_ -on your device. - Install monitoring packages on the device ----------------------------------------- `Install the openwrt-openwisp-monitoring packages -`_ +`_ on your device. These packages collect and send the monitoring data from the device to -OpenWISP Monitoring and are required to collect :ref:`metrics -` like interface traffic, WiFi clients, CPU -load, memory usage, etc. +OpenWISP Monitoring and are required to collect :doc:`metrics <./metrics>` +like interface traffic, WiFi clients, CPU load, memory usage, etc. -.. note:: - - If you are an existing user of *openwisp-monitoring* and are using the - legacy *monitoring template* for collecting metrics, we highly - recommend `Migrating from monitoring scripts to monitoring packages - <#migrating-from-monitoring-scripts-to-monitoring-packages>`_. +.. _openwisp_reach_devices: Make sure OpenWISP can reach your devices ----------------------------------------- -In order to perform `active checks <#available-checks>`_ and other actions -like `triggering the push of configuration changes -`_, -`executing shell commands -`_ -or `performing firmware upgrades -`_, -**the OpenWISP server needs to be able to reach the network devices**. +In order to perform :doc:`active checks <./checks>` and other actions like +:ref:`triggering the push of configuration changes +`, :ref:`executing shell commands +`, or :ref:`performing firmware upgrades +`, **the OpenWISP server needs to be able to reach the +network devices**. There are mainly two deployment scenarios for OpenWISP: @@ -61,15 +40,15 @@ This is the most common scenario: - the OpenWISP server is deployed to the public internet, hence the server has a public IPv4 (and IPv6) address and usually a valid SSL certificate - provided by Mozilla Letsencrypt or another SSL provider + provided by Let's Encrypt or another SSL provider - the network devices are geographically distributed across different locations (different cities, different regions, different countries) In this scenario, the OpenWISP application will not be able to reach the devices **unless a management tunnel** is used, for that reason having a -management VPN like OpenVPN, Wireguard or any other tunneling solution is -paramount, not only to allow OpenWISP to work properly, but also to be -able to perform debugging and troubleshooting when needed. +management VPN like OpenVPN, Wireguard, ZeroTier or any other tunneling +solution is paramount, not only to allow OpenWISP to work properly, but +also to be able to perform debugging and troubleshooting when needed. In this scenario, the following requirements are needed: @@ -77,9 +56,8 @@ In this scenario, the following requirements are needed: reach the VPN peers, for more information on how to do this via OpenWISP please refer to the following sections: - - `OpenVPN tunnel automation `_ - - `Wireguard tunnel automation - `_ + - :ref:`OpenVPN tunnel automation ` + - :ref:`Wireguard tunnel automation ` If you prefer to use other tunneling solutions (L2TP, Softether, etc.) and know how to configure those solutions on your own, that's totally @@ -94,8 +72,7 @@ In this scenario, the following requirements are needed: - The devices must be configured to join the management tunnel automatically, either via a pre-existing configuration in the firmware - or via an `OpenWISP Template - `_. + or via an :ref:`OpenWISP Template `. - The `openwisp-config `_ agent on the devices must be configured to specify the ``management_interface`` option, the agent will communicate the IP of @@ -122,15 +99,5 @@ LAN address, OpenWISP can then use the **Last IP** field of the devices to reach them. In this scenario it's necessary to set the -`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" -<#openwisp-monitoring-management-ip-only>`_ setting to ``False``. - -Creating checks for a device ----------------------------- - -By default, the `active checks <#available-checks>`_ are created -automatically for all devices, unless the automatic creation of some -specific checks has been disabled, for more information on how to do this, -refer to the `active checks <#available-checks>`_ section. - -These checks are created and executed in the background by celery workers. +:ref:`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" +` setting to ``False``. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 46528ea2..580e0e1d 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -1,35 +1,44 @@ -Rest API -======== +Rest API Reference +================== + +.. _monitoring_live_documentation: Live documentation ------------------ -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-doc.png +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-doc.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-doc.png A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. +.. _monitoring_browsable_web_interface: + Browsable web interface ----------------------- -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-1.png +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-ui-1.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-ui-1.png -.. image:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/api-ui-2.png +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-ui-2.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-ui-2.png -Additionally, opening any of the endpoints `listed below -<#list-of-endpoints>`_ directly in the browser will show the `browsable -API interface of Django-REST-Framework +Additionally, opening any of the endpoints :ref:`listed below +` directly in the browser will show the +`browsable API interface of Django-REST-Framework `_, which makes it even easier to find out the details of each endpoint. +.. _monitoring_rest_endpoints: + List of endpoints ----------------- -Since the detailed explanation is contained in the `Live documentation -<#live-documentation>`_ and in the `Browsable web page -<#browsable-web-interface>`_ of each point, here we'll provide just a list -of the available endpoints, for further information please open the URL of -the endpoint in your browser. +Since the detailed explanation is contained in the :ref:`Live +documentation ` and in the :ref:`Browsable +web page ` of each point, here we'll +provide just a list of the available endpoints, for further information +please open the URL of the endpoint in your browser. Retrieve general monitoring charts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -77,19 +86,19 @@ Retrieve device charts and device status data The format used for Device Status is inspired by `NetJSON DeviceMonitoring `_. -**Notes**: +.. note:: -- If the request is made without ``?status=true`` the response will - contain only charts data and will not include any device status - information (current load average, ARP table, DCHP leases, etc.). -- When retrieving chart data, the ``time`` parameter allows to specify the - time frame, eg: + - If the request is made without ``?status=true`` the response will + contain only charts data and will not include any device status + information (current load average, ARP table, DCHP leases, etc.). + - When retrieving chart data, the ``time`` parameter allows to specify + the time frame, eg: - - ``1d``: returns data of the last day - - ``3d``: returns data of the last 3 days - - ``7d``: returns data of the last 7 days - - ``30d``: returns data of the last 30 days - - ``365d``: returns data of the last 365 days + - ``1d``: returns data of the last day + - ``3d``: returns data of the last 3 days + - ``7d``: returns data of the last 7 days + - ``30d``: returns data of the last 30 days + - ``365d``: returns data of the last 365 days - In alternative to ``time`` it is possible to request chart data for a custom date range by using the ``start`` and ``end`` parameters, eg: @@ -98,7 +107,7 @@ The format used for Device Status is inspired by `NetJSON DeviceMonitoring and device status information (only if ``?status=true``). - This endpoint can be accessed with session authentication, token authentication, or alternatively with the device key passed as query - string parameter as shown below (`?key={key}`); note: this method is + string parameter as shown below (``?key={key}``); note: this method is meant to be used by the devices. .. code-block:: text @@ -117,14 +126,14 @@ List device monitoring information GET /api/v1/monitoring/device/ -**Notes**: +.. note:: -- The response contains device information and monitoring status (health - status), but it does not include the information and health status of - the specific metrics, this information can be retrieved in the detail - endpoint of each device. -- This endpoint can be accessed with session authentication and token - authentication. + - The response contains device information and monitoring status + (health status), but it does not include the information and health + status of the specific metrics, this information can be retrieved in + the detail endpoint of each device. + - This endpoint can be accessed with session authentication and token + authentication. **Available filters** @@ -242,8 +251,8 @@ Here's a few examples: .. note:: - Both `start_time` and `stop_time` support greater than or equal to, as - well as less than or equal to, filter lookups. + Both ``start_time`` and ``stop_time`` support greater than or equal + to, as well as less than or equal to, filter lookups. For example: diff --git a/docs/user/settings.rst b/docs/user/settings.rst index c2fa11c8..3afda248 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -1,6 +1,10 @@ Settings ======== +.. include:: /partials/settings-note.rst + +.. _timeseries_database: + ``TIMESERIES_DATABASE`` ----------------------- @@ -82,13 +86,15 @@ Timeseries database options database = "openwisp2" retention-policy = 'short' -If you are using `ansible-openwisp2 -`_ for deploying OpenWISP, -you can set the ``influxdb_udp_mode`` ansible variable to ``true`` in your -playbook, this will make the ansible role automatically configure the -InfluxDB UDP listeners. You can refer to the `ansible-ow-influxdb's -`_ (a -dependency of ansible-openwisp2) documentation to learn more. + If you are using `ansible-openwisp2 + `_ for deploying OpenWISP, + you can set the ``influxdb_udp_mode`` ansible variable to ``true`` in your + playbook, this will make the ansible role automatically configure the + InfluxDB UDP listeners. You can refer to the `ansible-ow-influxdb's + `_ (a + dependency of ansible-openwisp2) documentation to learn more. + +.. _openwisp_monitoring_default_retention_policy: ``OPENWISP_MONITORING_DEFAULT_RETENTION_POLICY`` ------------------------------------------------ @@ -100,6 +106,8 @@ dependency of ansible-openwisp2) documentation to learn more. The default retention policy that applies to the timeseries data. +.. _openwisp_monitoring_short_retention_policy: + ``OPENWISP_MONITORING_SHORT_RETENTION_POLICY`` ---------------------------------------------- @@ -182,10 +190,12 @@ following: **default**: ``True`` ============ ======== -This setting allows you to choose whether `config_applied -<#configuration-applied>`_ checks should be created automatically for +This setting allows you to choose whether :ref:`config_applied +` checks should be created automatically for newly registered devices. It's enabled by default. +.. _openwisp_monitoring_config_check_interval: + ``OPENWISP_MONITORING_CONFIG_CHECK_INTERVAL`` --------------------------------------------- @@ -195,7 +205,7 @@ newly registered devices. It's enabled by default. ============ ======= This setting allows you to configure the config check interval used by -`config_applied <#configuration-applied>`_. By default it is set to 5 +:ref:`config_applied `. By default it is set to 5 minutes. .. _openwisp_monitoring_auto_iperf3: @@ -208,8 +218,8 @@ minutes. **default**: ``False`` ============ ========= -This setting allows you to choose whether :ref:`iperf3 ` checks -should be created automatically for newly registered devices. It's +This setting allows you to choose whether :ref:`iperf3 ` +checks should be created automatically for newly registered devices. It's disabled by default. .. _openwisp_monitoring_iperf3_check_config: @@ -226,8 +236,8 @@ This setting allows to override the default iperf3 check configuration defined in ``openwisp_monitoring.check.classes.iperf3.DEFAULT_IPERF3_CHECK_CONFIG``. -For example, you can change the values of `supported iperf3 check -parameters <#iperf3-check-parameters>`_. +For example, you can change the values of :ref:`supported iperf3 check +parameters `. .. code-block:: python @@ -271,7 +281,7 @@ parameters <#iperf3-check-parameters>`_. ============ ======== This setting allows you to set whether :ref:`iperf3 check RSA public key -` will be deleted after successful +` will be deleted after successful completion of the check or not. ``OPENWISP_MONITORING_IPERF3_CHECK_LOCK_EXPIRE`` @@ -287,6 +297,8 @@ 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_charts: + ``OPENWISP_MONITORING_AUTO_CHARTS`` ----------------------------------- @@ -298,6 +310,8 @@ time. By default, it is set to **600 seconds (10 mins)**. Automatically created charts. +.. _openwisp_monitoring_critical_device_metrics: + ``OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS`` ----------------------------------------------- @@ -315,6 +329,8 @@ status of the device related to the metric moves into ``CRITICAL``. By default, if devices are not reachable by pings they are flagged as ``CRITICAL``. +.. _openwisp_monitoring_health_status_labels: + ``OPENWISP_MONITORING_HEALTH_STATUS_LABELS`` -------------------------------------------- @@ -346,8 +362,9 @@ want to use ``online`` instead of ``ok`` and ``offline`` instead of **default**: ``True`` ============ ======== -Setting this to ``False`` will disable `Monitoring Wifi Sessions -<#monitoring-wifi-sessions>`_ feature. +Setting this to ``False`` will disable :doc:`wifi-sessions` feature. + +.. _openwisp_monitoring_management_ip_only: ``OPENWISP_MONITORING_MANAGEMENT_IP_ONLY`` ------------------------------------------ @@ -367,11 +384,13 @@ layer2 network, hence the OpenWSP server can reach the devices using the .. note:: If this setting is not configured, it will fallback to the value of - `OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting - `_. + :ref:`OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting + `. If ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, then it will fallback to ``True``. +.. _openwisp_monitoring_device_recovery_detection: + ``OPENWISP_MONITORING_DEVICE_RECOVERY_DETECTION`` ------------------------------------------------- @@ -394,6 +413,8 @@ monitoring checks. For more information see: `Network Topology Device Integration `_ +.. _openwisp_monitoring_mac_vendor_detection: + ``OPENWISP_MONITORING_MAC_VENDOR_DETECTION`` -------------------------------------------- @@ -408,6 +429,8 @@ Identifier) table. This feature is enabled by default. +.. _openwisp_monitoring_write_retry_options: + ``OPENWISP_MONITORING_WRITE_RETRY_OPTIONS`` ------------------------------------------- @@ -447,6 +470,8 @@ documentation regarding automatic retries for known errors to the timeseries database. It is due to the nature of ``UDP`` protocol which does not acknowledge receipt of data packets. +.. _openwisp_monitoring_timeseries_retry_options: + ``OPENWISP_MONITORING_TIMESERIES_RETRY_OPTIONS`` ------------------------------------------------ @@ -476,6 +501,8 @@ fail. However these retries are not handled by celery but are simple python loops, which will eventually give up if a problem persists. +.. _openwisp_monitoring_timeseries_retry_delay: + ``OPENWISP_MONITORING_TIMESERIES_RETRY_DELAY`` ---------------------------------------------- @@ -492,6 +519,8 @@ timeseries database resilient. This setting is independent of celery retry settings. +.. _openwisp_monitoring_dashboard_map: + ``OPENWISP_MONITORING_DASHBOARD_MAP`` ------------------------------------- @@ -715,6 +744,8 @@ do it: "traffic": {"colors": ["#000000", "#cccccc", "#111111"]} } +.. _openwisp_monitoring_default_chart_time: + ``OPENWISP_MONITORING_DEFAULT_CHART_TIME`` ------------------------------------------ @@ -726,6 +757,8 @@ do it: Allows to set the default time period of the time series charts. +.. _openwisp_monitoring_auto_clear_management_ip: + ``OPENWISP_MONITORING_AUTO_CLEAR_MANAGEMENT_IP`` ------------------------------------------------ @@ -737,6 +770,8 @@ Allows to set the default time period of the time series charts. This setting allows you to automatically clear management_ip of a device when it goes offline. It is enabled by default. +.. _openwisp_monitoring_api_urlconf: + ``OPENWISP_MONITORING_API_URLCONF`` ----------------------------------- @@ -749,6 +784,8 @@ Changes the urlconf option of django urls to point the monitoring API urls to another installed module, example, ``myapp.urls``. (Useful when you have a seperate API instance.) +.. _openwisp_monitoring_api_baseurl: + ``OPENWISP_MONITORING_API_BASEURL`` ----------------------------------- @@ -762,6 +799,8 @@ different domain, you can use this option to change the base of the url, this will enable you to point all the API urls to your openwisp-monitoring API server's domain, example: ``https://mymonitoring.myapp.com``. +.. _openwisp_monitoring_cache_timeout: + ``OPENWISP_MONITORING_CACHE_TIMEOUT`` ------------------------------------- diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 0c2647a0..7794faf4 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -9,10 +9,12 @@ 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 +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/wifi-session-changelist.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/wifi-session-changelist.png :align: center -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/wifi-session-change.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/wifi-session-change.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/wifi-session-change.png :align: center You can disable this feature by configuring @@ -22,12 +24,22 @@ You can disable this feature by configuring You can also view open WiFi sessions of a device directly from the device's change admin under the "WiFi Sessions" tab. -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/device-wifi-session-inline.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-wifi-session-inline.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-wifi-session-inline.png :align: center Scheduled deletion of WiFi sessions ----------------------------------- +.. note:: + + If you have deployed OpenWISP using `ansible-openwisp2 + `_ or `docker-openwisp + `_, then this feature has + been already configured for you. This section is only for reference + for users who wish to customize OpenWISP, or who have deployed + OpenWISP in a different way. + 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`` From f415c68c0a405b2b244c64bebd689fe6ddde195c Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 29 May 2024 00:40:38 +0530 Subject: [PATCH 10/42] [docs] Restructured developer docs --- docs/developer/exceptions.rst | 33 -- docs/developer/management-commands.rst | 33 -- docs/developer/monitoring-scripts.rst | 55 -- .../registering-new-notification-types.rst | 15 - ...ring-unregistering-chart-configuration.rst | 115 ---- ...ing-unregistering-metric-configuration.rst | 183 ------- docs/developer/signals.rst | 96 ---- docs/developer/utils.rst | 497 ++++++++++++++++++ docs/user/checks.rst | 16 - docs/user/intro.rst | 12 +- docs/user/settings.rst | 19 +- 11 files changed, 513 insertions(+), 561 deletions(-) delete mode 100644 docs/developer/management-commands.rst delete mode 100644 docs/developer/monitoring-scripts.rst delete mode 100644 docs/developer/registering-new-notification-types.rst delete mode 100644 docs/developer/registering-unregistering-chart-configuration.rst delete mode 100644 docs/developer/registering-unregistering-metric-configuration.rst delete mode 100644 docs/developer/signals.rst create mode 100644 docs/developer/utils.rst diff --git a/docs/developer/exceptions.rst b/docs/developer/exceptions.rst index a8056471..8b137891 100644 --- a/docs/developer/exceptions.rst +++ b/docs/developer/exceptions.rst @@ -1,34 +1 @@ -Exceptions -========== -.. include:: /partials/developers-docs-warning.rst - -``TimeseriesWriteException`` ----------------------------- - -**Path**: ``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` - -If there is any failure due while writing data in timeseries database, -this exception shall be raised with a helpful error message explaining the -cause of the failure. This exception will normally be caught and the -failed write task will be retried in the background so that there is no -loss of data if failures occur due to overload of Timeseries server. You -can read more about this retry mechanism at -`OPENWISP_MONITORING_WRITE_RETRY_OPTIONS -<#openwisp-monitoring-write-retry-options>`_. - -``InvalidMetricConfigException`` --------------------------------- - -**Path**: -``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` - -This exception shall be raised if the metric configuration is broken. - -``InvalidChartConfigException`` -------------------------------- - -**Path**: -``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` - -This exception shall be raised if the chart configuration is broken. diff --git a/docs/developer/management-commands.rst b/docs/developer/management-commands.rst deleted file mode 100644 index 89b5b98b..00000000 --- a/docs/developer/management-commands.rst +++ /dev/null @@ -1,33 +0,0 @@ -Management commands -=================== - -.. include:: /partials/developers-docs-warning.rst - -.. _run_checks: - -``run_checks`` --------------- - -This command will execute all the `available checks `_ for all the -devices. By default checks are run periodically by *celery beat*. You can -learn more about this in :ref:`Setup -`. - -Example usage: - -.. code-block:: shell - - cd tests/ - ./manage.py run_checks - -``migrate_timeseries`` ----------------------- - -This command triggers asynchronous migration of the time-series database. - -Example usage: - -.. code-block:: shell - - cd tests/ - ./manage.py migrate_timeseries diff --git a/docs/developer/monitoring-scripts.rst b/docs/developer/monitoring-scripts.rst deleted file mode 100644 index c31bda55..00000000 --- a/docs/developer/monitoring-scripts.rst +++ /dev/null @@ -1,55 +0,0 @@ -Monitoring scripts -================== - -.. include:: /partials/developers-docs-warning.rst - -Monitoring scripts are now deprecated in favour of `monitoring packages -`_. -Follow the migration guide in `Migrating from monitoring scripts to -monitoring packages -<#migrating-from-monitoring-scripts-to-monitoring-packages>`_ section of -this documentation. - -Migrating from monitoring scripts to monitoring packages -======================================================== - -This section is intended for existing users of *openwisp-monitoring*. The -older version of *openwisp-monitoring* used *monitoring scripts* that are -now deprecated in favour of `monitoring packages -`_. - -If you already had a *monitoring template* created on your installation, -then the migrations of *openwisp-monitoring* will update that template by -making the following changes: - -- The file name of all scripts will be appended with ``legacy-`` keyword - in order to differentiate them from the scripts bundled with the new - packages. -- The ``/usr/sbin/legacy-openwisp-monitoring`` (previously - ``/usr/sbin/openwisp-monitoring``) script will be updated to exit if - `openwisp-monitoring package - `_ - is installed on the device. - -Install the `monitoring packages -`_ -as mentioned in the `Install monitoring packages on device -<#install-monitoring-packages-on-the-device>`_ section of this -documentation. - -After the proper configuration of the `openwisp-monitoring package -`_ -on your device, you can remove the monitoring template from your devices. - -We suggest removing the monitoring template from the devices one at a time -instead of deleting the template. This ensures the correctness of -*openwisp monitoring package* configuration and you'll not miss out on any -monitoring data. - -.. note:: - - If you have made changes to the default monitoring template created by - *openwisp-monitoring* or you are using custom monitoring templates, - then you should remove such templates from the device before - installing the `monitoring packages - `_. diff --git a/docs/developer/registering-new-notification-types.rst b/docs/developer/registering-new-notification-types.rst deleted file mode 100644 index 0c54b181..00000000 --- a/docs/developer/registering-new-notification-types.rst +++ /dev/null @@ -1,15 +0,0 @@ -Registering new notification types -================================== - -.. include:: /partials/developers-docs-warning.rst - -You can define your own notification types using -``register_notification_type`` function from OpenWISP Notifications. For -more information, see the relevant `openwisp-notifications section about -registering notification types -`_. - -Once a new notification type is registered, you have to use the `"notify" -signal provided in openwisp-notifications -`_ -to send notifications for this type. diff --git a/docs/developer/registering-unregistering-chart-configuration.rst b/docs/developer/registering-unregistering-chart-configuration.rst deleted file mode 100644 index 897af7ba..00000000 --- a/docs/developer/registering-unregistering-chart-configuration.rst +++ /dev/null @@ -1,115 +0,0 @@ -Registering / Unregistering Chart Configuration -=============================================== - -.. include:: /partials/developers-docs-warning.rst - -**OpenWISP Monitoring** provides registering and unregistering chart -configuration through utility functions -``openwisp_monitoring.monitoring.configuration.register_chart`` and -``openwisp_monitoring.monitoring.configuration.unregister_chart``. Using -these functions you can register or unregister chart configurations from -anywhere in your code. - -``register_chart`` ------------------- - -This function is used to register a new chart configuration from anywhere -in your code. - -======================== =============================================== -**Parameter** **Description** -**chart_name**: A ``str`` defining name of the chart - configuration. -**chart_configuration**: A ``dict`` defining configuration of the chart. -======================== =============================================== - -An example usage has been shown below. - -.. code-block:: python - - from openwisp_monitoring.monitoring.configuration import register_chart - - # Define configuration of your chart - chart_config = { - "type": "histogram", - "title": "Histogram", - "description": "Histogram", - "top_fields": 2, - "order": 999, - "query": { - "influxdb": ( - "SELECT {fields|SUM|/ 1} FROM {key} " - "WHERE time >= '{time}' AND content_type = " - "'{content_type}' AND object_id = '{object_id}'" - ) - }, - } - - # Register your custom chart configuration - register_chart("chart_name", chart_config) - -.. note:: - - It will raise ``ImproperlyConfigured`` exception if a chart - configuration is already registered with same name (not to be confused - with verbose_name). - -If you don't need to register a new chart but need to change a specific -key of an existing chart configuration, you can use -:ref:`OPENWISP_MONITORING_CHARTS `. - -Adaptive size charts -~~~~~~~~~~~~~~~~~~~~ - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/adaptive-chart.png - :align: center - -When configuring charts, it is possible to flag their unit as -``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `K`, `M`, `G` and `T` depending on the size -of each point, the summary values and Y axis are also resized. - -Example taken from the default configuration of the traffic chart: - -.. code-block:: python - - OPENWISP_MONITORING_CHARTS = { - "traffic": { - # other configurations for this chart - # traffic measured in 'B' (bytes) - # unit B, KB, MB, GB, TB - "unit": "adaptive_prefix+B", - }, - "bandwidth": { - # other configurations for this chart - # adaptive unit for bandwidth related charts - # bandwidth measured in 'bps'(bits/sec) - # unit bps, Kbps, Mbps, Gbps, Tbps - "unit": "adaptive_prefix+bps", - }, - } - -``unregister_chart`` --------------------- - -This function is used to unregister a chart configuration from anywhere in -your code. - -=============== =================================================== -**Parameter** **Description** -**chart_name**: A ``str`` defining name of the chart configuration. -=============== =================================================== - -An example usage is shown below. - -.. code-block:: python - - from openwisp_monitoring.monitoring.configuration import unregister_chart - - # Unregister previously registered chart configuration - unregister_chart("chart_name") - -.. note:: - - It will raise ``ImproperlyConfigured`` exception if the concerned - chart configuration is not registered. diff --git a/docs/developer/registering-unregistering-metric-configuration.rst b/docs/developer/registering-unregistering-metric-configuration.rst deleted file mode 100644 index 3a933bd3..00000000 --- a/docs/developer/registering-unregistering-metric-configuration.rst +++ /dev/null @@ -1,183 +0,0 @@ -Registering / Unregistering Metric Configuration -================================================ - -.. include:: /partials/developers-docs-warning.rst - -**OpenWISP Monitoring** provides registering and unregistering metric -configuration through utility functions -``openwisp_monitoring.monitoring.configuration.register_metric`` and -``openwisp_monitoring.monitoring.configuration.unregister_metric``. Using -these functions you can register or unregister metric configurations from -anywhere in your code. - -``register_metric`` -------------------- - -This function is used to register a new metric configuration from anywhere -in your code. - -========================= ================================================ -**Parameter** **Description** -**metric_name**: A ``str`` defining name of the metric - configuration. -**metric_configuration**: A ``dict`` defining configuration of the metric. -========================= ================================================ - -An example usage has been shown below. - -.. code-block:: python - - from django.utils.translation import gettext_lazy as _ - from openwisp_monitoring.monitoring.configuration import register_metric - - # Define configuration of your metric - metric_config = { - "label": _("Ping"), - "name": "Ping", - "key": "ping", - "field_name": "reachable", - "related_fields": ["loss", "rtt_min", "rtt_max", "rtt_avg"], - "charts": { - "uptime": { - "type": "bar", - "title": _("Uptime"), - "description": _( - "A value of 100% means reachable, 0% means unreachable, values in " - "between 0% and 100% indicate the average reachability in the " - "period observed. Obtained with the fping linux program." - ), - "summary_labels": [_("Average uptime")], - "unit": "%", - "order": 200, - "colorscale": { - "max": 100, - "min": 0, - "label": _("Reachable"), - "scale": [ - [ - [0, "#c13000"], - [0.1, "cb7222"], - [0.5, "#deed0e"], - [0.9, "#7db201"], - [1, "#498b26"], - ], - ], - "map": [ - [100, "#498b26", _("Reachable")], - [90, "#7db201", _("Mostly Reachable")], - [50, "#deed0e", _("Partly Reachable")], - [10, "#cb7222", _("Mostly Unreachable")], - [None, "#c13000", _("Unreachable")], - ], - "fixed_value": 100, - }, - "query": chart_query["uptime"], - }, - "packet_loss": { - "type": "bar", - "title": _("Packet loss"), - "description": _( - "Indicates the percentage of lost packets observed in ICMP probes. " - "Obtained with the fping linux program." - ), - "summary_labels": [_("Average packet loss")], - "unit": "%", - "colors": "#d62728", - "order": 210, - "query": chart_query["packet_loss"], - }, - "rtt": { - "type": "scatter", - "title": _("Round Trip Time"), - "description": _( - "Round trip time observed in ICMP probes, measuered in milliseconds." - ), - "summary_labels": [ - _("Average RTT"), - _("Average Max RTT"), - _("Average Min RTT"), - ], - "unit": _(" ms"), - "order": 220, - "query": chart_query["rtt"], - }, - }, - "alert_settings": {"operator": "<", "threshold": 1, "tolerance": 0}, - "notification": { - "problem": { - "verbose_name": "Ping PROBLEM", - "verb": "cannot be reached anymore", - "level": "warning", - "email_subject": _( - "[{site.name}] {notification.target} is not reachable" - ), - "message": _( - "The device [{notification.target}] {notification.verb} anymore by our ping " - "messages." - ), - }, - "recovery": { - "verbose_name": "Ping RECOVERY", - "verb": "has become reachable", - "level": "info", - "email_subject": _( - "[{site.name}] {notification.target} is reachable again" - ), - "message": _( - "The device [{notification.target}] {notification.verb} again by our ping " - "messages." - ), - }, - }, - } - - # Register your custom metric configuration - register_metric("ping", metric_config) - -The above example will register one metric configuration (named ``ping``), -three chart configurations (named ``rtt``, ``packet_loss``, ``uptime``) as -defined in the **charts** key, two notification types (named -``ping_recovery``, ``ping_problem``) as defined in **notification** key. - -The ``AlertSettings`` of ``ping`` metric will by default use ``threshold`` -and ``tolerance`` defined in the ``alert_settings`` key. You can always -override them and define your own custom values via the *admin*. - -You can also use the ``alert_field`` key in metric configuration which -allows ``AlertSettings`` to check the ``threshold`` on ``alert_field`` -instead of the default ``field_name`` key. - -.. note:: - - It will raise ``ImproperlyConfigured`` exception if a metric - configuration is already registered with same name (not to be confused - with verbose_name). - -If you don't need to register a new metric but need to change a specific -key of an existing metric configuration, you can use -:ref:`OPENWISP_MONITORING_METRICS `. - -``unregister_metric`` ---------------------- - -This function is used to unregister a metric configuration from anywhere -in your code. - -================ ==================================================== -**Parameter** **Description** -**metric_name**: A ``str`` defining name of the metric configuration. -================ ==================================================== - -An example usage is shown below. - -.. code-block:: python - - from openwisp_monitoring.monitoring.configuration import unregister_metric - - # Unregister previously registered metric configuration - unregister_metric("metric_name") - -.. note:: - - It will raise ``ImproperlyConfigured`` exception if the concerned - metric configuration is not registered. diff --git a/docs/developer/signals.rst b/docs/developer/signals.rst deleted file mode 100644 index bb742445..00000000 --- a/docs/developer/signals.rst +++ /dev/null @@ -1,96 +0,0 @@ -Signals -======= - -.. include:: /partials/developers-docs-warning.rst - -``device_metrics_received`` ---------------------------- - -**Path**: ``openwisp_monitoring.device.signals.device_metrics_received`` - -**Arguments**: - -- ``instance``: instance of ``Device`` whose metrics have been received -- ``request``: the HTTP request object -- ``time``: time with which metrics will be saved. If none, then server - time will be used -- ``current``: whether the data has just been collected or was collected - previously and sent now due to network connectivity issues - -This signal is emitted when device metrics are received to the -``DeviceMetric`` view (only when using HTTP POST). - -The signal is emitted just before a successful response is returned, it is -not sent if the response was not successful. - -``health_status_changed`` -------------------------- - -**Path**: ``openwisp_monitoring.device.signals.health_status_changed`` - -**Arguments**: - -- ``instance``: instance of ``DeviceMonitoring`` whose status has been - changed -- ``status``: the status by which DeviceMonitoring's existing status has - been updated with - -This signal is emitted only if the health status of DeviceMonitoring -object gets updated. - -``threshold_crossed`` ---------------------- - -**Path**: ``openwisp_monitoring.monitoring.signals.threshold_crossed`` - -**Arguments**: - -- ``metric``: ``Metric`` object whose threshold defined in related alert - settings was crossed -- ``alert_settings``: ``AlertSettings`` related to the ``Metric`` -- ``target``: related ``Device`` object -- ``first_time``: it will be set to true when the metric is written for - the first time. It shall be set to false afterwards. -- ``tolerance_crossed``: it will be set to true if the metric has crossed - the threshold for tolerance configured in alert settings. Otherwise, it - will be set to false. - -``first_time`` parameter can be used to avoid initiating unneeded actions. -For example, sending recovery notifications. - -This signal is emitted when the threshold value of a ``Metric`` defined in -alert settings is crossed. - -``pre_metric_write`` --------------------- - -**Path**: ``openwisp_monitoring.monitoring.signals.pre_metric_write`` - -**Arguments**: - -- ``metric``: ``Metric`` object whose data shall be stored in timeseries - database -- ``values``: metric data that shall be stored in the timeseries database -- ``time``: time with which metrics will be saved -- ``current``: whether the data has just been collected or was collected - previously and sent now due to network connectivity issues - -This signal is emitted for every metric before the write operation is sent -to the timeseries database. - -``post_metric_write`` ---------------------- - -**Path**: ``openwisp_monitoring.monitoring.signals.post_metric_write`` - -**Arguments**: - -- ``metric``: ``Metric`` object whose data is being stored in timeseries - database -- ``values``: metric data that is being stored in the timeseries database -- ``time``: time with which metrics will be saved -- ``current``: whether the data has just been collected or was collected - previously and sent now due to network connectivity issues - -This signal is emitted for every metric after the write operation is -successfully executed in the background. diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst new file mode 100644 index 00000000..d04de597 --- /dev/null +++ b/docs/developer/utils.rst @@ -0,0 +1,497 @@ +Code Utilities +============== + +.. include:: ../partials/developer-docs.rst + +.. contents:: + :depth: 2 + :local: + +Registering / Unregistering Metric Configuration +------------------------------------------------ + +**OpenWISP Monitoring** provides registering and unregistering metric +configuration through utility functions +``openwisp_monitoring.monitoring.configuration.register_metric`` and +``openwisp_monitoring.monitoring.configuration.unregister_metric``. Using +these functions you can register or unregister metric configurations from +anywhere in your code. + +``register_metric`` +~~~~~~~~~~~~~~~~~~~ + +This function is used to register a new metric configuration from anywhere +in your code. + +========================= ================================================ +**Parameter** **Description** +**metric_name**: A ``str`` defining name of the metric + configuration. +**metric_configuration**: A ``dict`` defining configuration of the metric. +========================= ================================================ + +An example usage has been shown below. + +.. code-block:: python + + from django.utils.translation import gettext_lazy as _ + from openwisp_monitoring.monitoring.configuration import register_metric + + # Define configuration of your metric + metric_config = { + "label": _("Ping"), + "name": "Ping", + "key": "ping", + "field_name": "reachable", + "related_fields": ["loss", "rtt_min", "rtt_max", "rtt_avg"], + "charts": { + "uptime": { + "type": "bar", + "title": _("Uptime"), + "description": _( + "A value of 100% means reachable, 0% means unreachable, values in " + "between 0% and 100% indicate the average reachability in the " + "period observed. Obtained with the fping linux program." + ), + "summary_labels": [_("Average uptime")], + "unit": "%", + "order": 200, + "colorscale": { + "max": 100, + "min": 0, + "label": _("Reachable"), + "scale": [ + [ + [0, "#c13000"], + [0.1, "cb7222"], + [0.5, "#deed0e"], + [0.9, "#7db201"], + [1, "#498b26"], + ], + ], + "map": [ + [100, "#498b26", _("Reachable")], + [90, "#7db201", _("Mostly Reachable")], + [50, "#deed0e", _("Partly Reachable")], + [10, "#cb7222", _("Mostly Unreachable")], + [None, "#c13000", _("Unreachable")], + ], + "fixed_value": 100, + }, + "query": chart_query["uptime"], + }, + "packet_loss": { + "type": "bar", + "title": _("Packet loss"), + "description": _( + "Indicates the percentage of lost packets observed in ICMP probes. " + "Obtained with the fping linux program." + ), + "summary_labels": [_("Average packet loss")], + "unit": "%", + "colors": "#d62728", + "order": 210, + "query": chart_query["packet_loss"], + }, + "rtt": { + "type": "scatter", + "title": _("Round Trip Time"), + "description": _( + "Round trip time observed in ICMP probes, measuered in milliseconds." + ), + "summary_labels": [ + _("Average RTT"), + _("Average Max RTT"), + _("Average Min RTT"), + ], + "unit": _(" ms"), + "order": 220, + "query": chart_query["rtt"], + }, + }, + "alert_settings": {"operator": "<", "threshold": 1, "tolerance": 0}, + "notification": { + "problem": { + "verbose_name": "Ping PROBLEM", + "verb": "cannot be reached anymore", + "level": "warning", + "email_subject": _( + "[{site.name}] {notification.target} is not reachable" + ), + "message": _( + "The device [{notification.target}] {notification.verb} anymore by our ping " + "messages." + ), + }, + "recovery": { + "verbose_name": "Ping RECOVERY", + "verb": "has become reachable", + "level": "info", + "email_subject": _( + "[{site.name}] {notification.target} is reachable again" + ), + "message": _( + "The device [{notification.target}] {notification.verb} again by our ping " + "messages." + ), + }, + }, + } + + # Register your custom metric configuration + register_metric("ping", metric_config) + +The above example will register one metric configuration (named ``ping``), +three chart configurations (named ``rtt``, ``packet_loss``, ``uptime``) as +defined in the **charts** key, two notification types (named +``ping_recovery``, ``ping_problem``) as defined in **notification** key. + +The ``AlertSettings`` of ``ping`` metric will by default use ``threshold`` +and ``tolerance`` defined in the ``alert_settings`` key. You can always +override them and define your own custom values via the *admin*. + +You can also use the ``alert_field`` key in metric configuration which +allows ``AlertSettings`` to check the ``threshold`` on ``alert_field`` +instead of the default ``field_name`` key. + +.. note:: + + It will raise ``ImproperlyConfigured`` exception if a metric + configuration is already registered with same name (not to be confused + with verbose_name). + +If you don't need to register a new metric but need to change a specific +key of an existing metric configuration, you can use +:ref:`OPENWISP_MONITORING_METRICS `. + +``unregister_metric`` +~~~~~~~~~~~~~~~~~~~~~ + +This function is used to unregister a metric configuration from anywhere +in your code. + +================ ==================================================== +**Parameter** **Description** +**metric_name**: A ``str`` defining name of the metric configuration. +================ ==================================================== + +An example usage is shown below. + +.. code-block:: python + + from openwisp_monitoring.monitoring.configuration import unregister_metric + + # Unregister previously registered metric configuration + unregister_metric("metric_name") + +.. note:: + + It will raise ``ImproperlyConfigured`` exception if the concerned + metric configuration is not registered. + +Registering / Unregistering Chart Configuration +----------------------------------------------- + +**OpenWISP Monitoring** provides registering and unregistering chart +configuration through utility functions +``openwisp_monitoring.monitoring.configuration.register_chart`` and +``openwisp_monitoring.monitoring.configuration.unregister_chart``. Using +these functions you can register or unregister chart configurations from +anywhere in your code. + +``register_chart`` +~~~~~~~~~~~~~~~~~~ + +This function is used to register a new chart configuration from anywhere +in your code. + +======================== =============================================== +**Parameter** **Description** +**chart_name**: A ``str`` defining name of the chart + configuration. +**chart_configuration**: A ``dict`` defining configuration of the chart. +======================== =============================================== + +An example usage has been shown below. + +.. code-block:: python + + from openwisp_monitoring.monitoring.configuration import register_chart + + # Define configuration of your chart + chart_config = { + "type": "histogram", + "title": "Histogram", + "description": "Histogram", + "top_fields": 2, + "order": 999, + "query": { + "influxdb": ( + "SELECT {fields|SUM|/ 1} FROM {key} " + "WHERE time >= '{time}' AND content_type = " + "'{content_type}' AND object_id = '{object_id}'" + ) + }, + } + + # Register your custom chart configuration + register_chart("chart_name", chart_config) + +.. note:: + + It will raise ``ImproperlyConfigured`` exception if a chart + configuration is already registered with same name (not to be confused + with verbose_name). + +If you don't need to register a new chart but need to change a specific +key of an existing chart configuration, you can use +:ref:`OPENWISP_MONITORING_CHARTS `. + +Adaptive size charts +++++++++++++++++++++ + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png + :align: center + +When configuring charts, it is possible to flag their unit as +``adaptive_prefix``, this allows to make the charts more readable because +the units are shown in either `K`, `M`, `G` and `T` depending on the size +of each point, the summary values and Y axis are also resized. + +Example taken from the default configuration of the traffic chart: + +.. code-block:: python + + OPENWISP_MONITORING_CHARTS = { + "traffic": { + # other configurations for this chart + # traffic measured in 'B' (bytes) + # unit B, KB, MB, GB, TB + "unit": "adaptive_prefix+B", + }, + "bandwidth": { + # other configurations for this chart + # adaptive unit for bandwidth related charts + # bandwidth measured in 'bps'(bits/sec) + # unit bps, Kbps, Mbps, Gbps, Tbps + "unit": "adaptive_prefix+bps", + }, + } + +``unregister_chart`` +~~~~~~~~~~~~~~~~~~~~ + +This function is used to unregister a chart configuration from anywhere in +your code. + +=============== =================================================== +**Parameter** **Description** +**chart_name**: A ``str`` defining name of the chart configuration. +=============== =================================================== + +An example usage is shown below. + +.. code-block:: python + + from openwisp_monitoring.monitoring.configuration import unregister_chart + + # Unregister previously registered chart configuration + unregister_chart("chart_name") + +.. note:: + + It will raise ``ImproperlyConfigured`` exception if the concerned + chart configuration is not registered. + +Monitoring Notifications +------------------------ + +OpenWISP Monitoring registers and uses the following notification types: + +- ``threshold_crossed``: Fires when a metric crosses the boundary defined + in the threshold value of the alert settings. +- ``threhold_recovery``: Fires when a metric goes back within the expected + range. +- ``connection_is_working``: Fires when the connection to a device is + working. +- ``connection_is_not_working``: Fires when the connection (eg: SSH) to a + device stops working (eg: credentials are outdated, management IP + address is outdated, or device is not reachable). + +Registering Notification Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can define your own notification types using +``register_notification_type`` function from OpenWISP Notifications. + +For more information, see the relevant :doc:`documentation section about +registering notification types in the Notifications module +`. + +Once a new notification type is registered, you have to use the +:doc:`"notify" signal provided the Notifications module +` to send notifications +for this type. + +Signals +------- + +.. include:: /partials/signals-note.rst + +``device_metrics_received`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.device.signals.device_metrics_received`` + +**Arguments**: + +- ``instance``: instance of ``Device`` whose metrics have been received +- ``request``: the HTTP request object +- ``time``: time with which metrics will be saved. If none, then server + time will be used +- ``current``: whether the data has just been collected or was collected + previously and sent now due to network connectivity issues + +This signal is emitted when device metrics are received to the +``DeviceMetric`` view (only when using HTTP POST). + +The signal is emitted just before a successful response is returned, it is +not sent if the response was not successful. + +``health_status_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.device.signals.health_status_changed`` + +**Arguments**: + +- ``instance``: instance of ``DeviceMonitoring`` whose status has been + changed +- ``status``: the status by which DeviceMonitoring's existing status has + been updated with + +This signal is emitted only if the health status of DeviceMonitoring +object gets updated. + +``threshold_crossed`` +~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.signals.threshold_crossed`` + +**Arguments**: + +- ``metric``: ``Metric`` object whose threshold defined in related alert + settings was crossed +- ``alert_settings``: ``AlertSettings`` related to the ``Metric`` +- ``target``: related ``Device`` object +- ``first_time``: it will be set to true when the metric is written for + the first time. It shall be set to false afterwards. +- ``tolerance_crossed``: it will be set to true if the metric has crossed + the threshold for tolerance configured in alert settings. Otherwise, it + will be set to false. + +``first_time`` parameter can be used to avoid initiating unneeded actions. +For example, sending recovery notifications. + +This signal is emitted when the threshold value of a ``Metric`` defined in +alert settings is crossed. + +``pre_metric_write`` +~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.signals.pre_metric_write`` + +**Arguments**: + +- ``metric``: ``Metric`` object whose data shall be stored in timeseries + database +- ``values``: metric data that shall be stored in the timeseries database +- ``time``: time with which metrics will be saved +- ``current``: whether the data has just been collected or was collected + previously and sent now due to network connectivity issues + +This signal is emitted for every metric before the write operation is sent +to the timeseries database. + +``post_metric_write`` +~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.monitoring.signals.post_metric_write`` + +**Arguments**: + +- ``metric``: ``Metric`` object whose data is being stored in timeseries + database +- ``values``: metric data that is being stored in the timeseries database +- ``time``: time with which metrics will be saved +- ``current``: whether the data has just been collected or was collected + previously and sent now due to network connectivity issues + +This signal is emitted for every metric after the write operation is +successfully executed in the background. + +Management commands +------------------- + +.. _run_checks: + +``run_checks`` +~~~~~~~~~~~~~~ + +This command will execute all the `available checks `_ for all the +devices. By default checks are run periodically by *celery beat*. You can +learn more about this in :ref:`Setup +`. + +Example usage: + +.. code-block:: shell + + cd tests/ + ./manage.py run_checks + +``migrate_timeseries`` +~~~~~~~~~~~~~~~~~~~~~~ + +This command triggers asynchronous migration of the time-series database. + +Example usage: + +.. code-block:: shell + + cd tests/ + ./manage.py migrate_timeseries + +Exceptions +---------- + +``TimeseriesWriteException`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` + +If there is any failure due while writing data in timeseries database, +this exception shall be raised with a helpful error message explaining the +cause of the failure. This exception will normally be caught and the +failed write task will be retried in the background so that there is no +loss of data if failures occur due to overload of Timeseries server. You +can read more about this retry mechanism at +`OPENWISP_MONITORING_WRITE_RETRY_OPTIONS +<#openwisp-monitoring-write-retry-options>`_. + +``InvalidMetricConfigException`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: +``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` + +This exception shall be raised if the metric configuration is broken. + +``InvalidChartConfigException`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: +``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` + +This exception shall be raised if the chart configuration is broken. diff --git a/docs/user/checks.rst b/docs/user/checks.rst index a5af2798..d96a85ee 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -58,19 +58,3 @@ rsa_publc_key etc) using the may need to update the :ref:`metric configuration ` to enable alerts for the iperf3 check. - -Alerts / Notifications ----------------------- - -The following kind of notifications will be sent based on the check -results: - -- ``threshold_crossed``: Fires when a metric crosses the boundary defined - in the threshold value of the alert settings. -- ``threhold_recovery``: Fires when a metric goes back within the expected - range. -- ``connection_is_working``: Fires when the connection to a device is - working. -- ``connection_is_not_working``: Fires when the connection (eg: SSH) to a - device stops working (eg: credentials are outdated, management IP - address is outdated, or device is not reachable). diff --git a/docs/user/intro.rst b/docs/user/intro.rst index acb8d8f7..02ae4e44 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -10,12 +10,12 @@ OpenWISP provides the following monitoring capabilities: like uptime, RAM status, CPU load averages, Interface properties and addresses, WiFi interface status and associated clients, Neighbors information, DHCP Leases, Disk/Flash status -- Monitoring charts for :ref:`uptime `, :ref:`packet loss `, - :ref:`round trip time (latency) `, :ref:`associated wifi clients - `, :ref:`interface traffic `, :ref:`RAM usage - `, :ref:`CPU load `, :ref:`flash/disk usage - `, mobile signal (LTE/UMTS/GSM :ref:`signal strength - `, :ref:`signal quality +- Monitoring charts for :ref:`uptime `, :ref:`packet loss + `, :ref:`round trip time (latency) `, + :ref:`associated wifi clients `, :ref:`interface traffic + `, :ref:`RAM usage `, :ref:`CPU load `, + :ref:`flash/disk usage `, mobile signal (LTE/UMTS/GSM + :ref:`signal strength `, :ref:`signal quality `, :ref:`access technology in use `), :ref:`bandwidth `, :ref:`transferred data `, :ref:`restransmits `, diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 3afda248..ee469956 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -87,10 +87,11 @@ Timeseries database options retention-policy = 'short' If you are using `ansible-openwisp2 - `_ for deploying OpenWISP, - you can set the ``influxdb_udp_mode`` ansible variable to ``true`` in your - playbook, this will make the ansible role automatically configure the - InfluxDB UDP listeners. You can refer to the `ansible-ow-influxdb's + `_ for deploying + OpenWISP, you can set the ``influxdb_udp_mode`` ansible variable to + ``true`` in your playbook, this will make the ansible role + automatically configure the InfluxDB UDP listeners. You can refer to + the `ansible-ow-influxdb's `_ (a dependency of ansible-openwisp2) documentation to learn more. @@ -191,8 +192,8 @@ following: ============ ======== This setting allows you to choose whether :ref:`config_applied -` checks should be created automatically for -newly registered devices. It's enabled by default. +` checks should be created automatically for newly +registered devices. It's enabled by default. .. _openwisp_monitoring_config_check_interval: @@ -385,9 +386,9 @@ layer2 network, hence the OpenWSP server can reach the devices using the If this setting is not configured, it will fallback to the value of :ref:`OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY setting - `. - If ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, - then it will fallback to ``True``. + `. If + ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` also not configured, then + it will fallback to ``True``. .. _openwisp_monitoring_device_recovery_detection: From 3c8bd3de851cc40f8ab8a27b170235df2979b5f8 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 29 May 2024 11:46:18 +0530 Subject: [PATCH 11/42] [docs] Restructured developer docs --- docs/developer/exceptions.rst | 1 - docs/developer/extending.rst | 6 +- docs/developer/index.rst | 4 +- docs/developer/installation.rst | 110 +++++++++++++------------------- docs/developer/utils.rst | 4 +- docs/user/quickstart.rst | 4 +- docs/user/wifi-sessions.rst | 20 +++--- 7 files changed, 62 insertions(+), 87 deletions(-) delete mode 100644 docs/developer/exceptions.rst diff --git a/docs/developer/exceptions.rst b/docs/developer/exceptions.rst deleted file mode 100644 index 8b137891..00000000 --- a/docs/developer/exceptions.rst +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index c768447b..55991027 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,7 +1,7 @@ -Extending openwisp-monitoring +Extending OpenWISP Monitoring ============================= -.. include:: /partials/developers-docs-warning.rst +.. include:: ../partials/developer-docs.rst One of the core values of the OpenWISP project is `Software Reusability `_, @@ -69,7 +69,7 @@ removed: For more information about how to work with django projects and django apps, please refer to the `"Tutorial: Writing your first Django app" in -the django docunmentation +the django documentation `_. 2. Install ``openwisp-monitoring`` diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 20098027..99915fc9 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -1,5 +1,5 @@ -Developers Documentation -======================== +Developer Docs +============== .. include:: ../partials/developer-docs.rst diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index bcb67046..a0975495 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,76 +1,30 @@ Installation instructions ========================= -.. include:: /partials/developers-docs-warning.rst +.. include:: ../partials/developer-docs.rst -Deploy it in production ------------------------ +Dependencies +------------ -See: - -- `ansible-openwisp2 `_ -- `docker-openwisp `_ - -.. _setup-integrate-in-an-existing-django-project: - -Install system dependencies ---------------------------- - -*openwisp-monitoring* uses InfluxDB to store metrics. Follow the -`installation instructions from InfluxDB's official documentation -`_. - -.. important:: - - Only *InfluxDB 1.8.x* is supported in *openwisp-monitoring*. - -Install system packages: - -.. code-block:: shell - - sudo apt install -y openssl libssl-dev \ - gdal-bin libproj-dev libgeos-dev \ - fping - -Install stable version from PyPI --------------------------------- - -Install from PyPI: - -.. code-block:: shell - - pip install openwisp-monitoring - -Install development version ---------------------------- - -Install tarball: - -.. code-block:: shell - - pip install https://github.com/openwisp/openwisp-monitoring/tarball/master - -Alternatively, you can install via pip using git: - -.. code-block:: shell - - pip install -e git+git://github.com/openwisp/openwisp-monitoring#egg=openwisp_monitoring - -If you want to contribute, follow the instructions in `"Installing for -development" <#installing-for-development>`_ section. +- Python >= 3.8 +- InfluxDB 1.8 +- fping +- OpenSSL Installing for development -------------------------- -Install the system dependencies as mentioned in the `"Install system -dependencies" <#install-system-dependencies>`_ section. Install these -additional packages that are required for development: +Install the system dependencies: + +Install system packages: .. code-block:: shell - sudo apt install -y sqlite3 libsqlite3-dev \ - libspatialite-dev libsqlite3-mod-spatialite \ - chromium + sudo apt update + sudo apt install -y sqlite3 libsqlite3-dev openssl libssl-dev + sudo apt install -y gdal-bin libproj-dev libgeos-dev libspatialite-dev libsqlite3-mod-spatialite + sudo apt install -y fping + sudo apt install -y chromium Fork and clone the forked repository: @@ -154,13 +108,41 @@ Run quality assurance tests with: ./run-qa-checks +Alternative sources +------------------- + +Pypi +~~~~ + +To install the latest stable version from pypi: + +.. code-block:: shell + + pip install openwisp-monitoring + +Github +~~~~~~ + +To install the latest development version tarball via HTTPs: + +.. code-block:: shell + + pip install https://github.com/openwisp/openwisp-monitoring/tarball/master + +Alternatively you can use the git protocol: + +.. code-block:: shell + + pip install -e git+git://github.com/openwisp/openwisp-monitoring#egg=openwisp_monitoring + Install and run on docker ------------------------- -.. note:: +.. warning:: + + This Docker image is for development purposes only. - This Docker image is for development purposes only. For the official - OpenWISP Docker images, see: `docker-openwisp + For the official OpenWISP Docker images, see: `docker-openwisp `_. Build from the Dockerfile: diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index d04de597..d395e459 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -440,9 +440,7 @@ Management commands ~~~~~~~~~~~~~~ This command will execute all the `available checks `_ for all the -devices. By default checks are run periodically by *celery beat*. You can -learn more about this in :ref:`Setup -`. +devices. By default checks are run periodically by *celery beat*. Example usage: diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 07643079..80a777ff 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -56,8 +56,8 @@ In this scenario, the following requirements are needed: reach the VPN peers, for more information on how to do this via OpenWISP please refer to the following sections: - - :ref:`OpenVPN tunnel automation ` - - :ref:`Wireguard tunnel automation ` + - :doc:`OpenVPN tunnel automation ` + - :doc:`Wireguard tunnel automation ` If you prefer to use other tunneling solutions (L2TP, Softether, etc.) and know how to configure those solutions on your own, that's totally diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 7794faf4..6988d42e 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -31,20 +31,18 @@ device's change admin under the "WiFi Sessions" tab. Scheduled deletion of WiFi sessions ----------------------------------- +OpenWISP Monitoring provides a celery task to automatically delete WiFi +sessions older than a pre-configured number of days. + .. note:: If you have deployed OpenWISP using `ansible-openwisp2 `_ or `docker-openwisp `_, then this feature has - been already configured for you. This section is only for reference - for users who wish to customize OpenWISP, or who have deployed - OpenWISP in a different way. - -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 :ref:`setup instructions -`. + been already configured for you. Refer to the documentation of your + deployment method to know the default value. This section is only for + reference for users who wish to customize OpenWISP, or who have + deployed OpenWISP in a different way. The celery task takes only one argument, i.e. number of days. You can provide any number of days in `args` key while configuring @@ -59,9 +57,7 @@ automatically, then configure ``CELERY_BEAT_SCHEDULE`` as follows: "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 + "args": (30,), # Here we have defined 30 days }, } From 441a8da5275ac806ab7b1a0d6c5de00a3964544d Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 29 May 2024 15:21:54 +0530 Subject: [PATCH 12/42] [docs] Fixed URLs --- docs/user/configuring-iperf3-check.rst | 4 ++-- docs/user/quickstart.rst | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/user/configuring-iperf3-check.rst b/docs/user/configuring-iperf3-check.rst index 73d543b9..0643671a 100644 --- a/docs/user/configuring-iperf3-check.rst +++ b/docs/user/configuring-iperf3-check.rst @@ -16,8 +16,8 @@ device, eg: 2. Ensure SSH access from OpenWISP is enabled on your devices ------------------------------------------------------------- -Follow the steps in :ref:`"Configuring Push Operations" -` section of the documentation to allow SSH +Follow the steps in :doc:`"Configuring Push Operations" +` section of the documentation to allow SSH access to you device from OpenWISP. .. important:: diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 80a777ff..055fe128 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -18,10 +18,10 @@ Make sure OpenWISP can reach your devices ----------------------------------------- In order to perform :doc:`active checks <./checks>` and other actions like -:ref:`triggering the push of configuration changes -`, :ref:`executing shell commands -`, or :ref:`performing firmware upgrades -`, **the OpenWISP server needs to be able to reach the +:doc:`triggering the push of configuration changes +`, :doc:`executing shell commands +`, or :doc:`performing firmware upgrades +`, **the OpenWISP server needs to be able to reach the network devices**. There are mainly two deployment scenarios for OpenWISP: @@ -72,7 +72,7 @@ In this scenario, the following requirements are needed: - The devices must be configured to join the management tunnel automatically, either via a pre-existing configuration in the firmware - or via an :ref:`OpenWISP Template `. + or via an :doc:`OpenWISP Template `. - The `openwisp-config `_ agent on the devices must be configured to specify the ``management_interface`` option, the agent will communicate the IP of From e3f2e8735137b64fad61659fd960d4740d9040c0 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 1 Jun 2024 14:05:08 -0400 Subject: [PATCH 13/42] [docs] Adaptive size improvement --- docs/developer/utils.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index d395e459..b528432c 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -256,7 +256,7 @@ Adaptive size charts When configuring charts, it is possible to flag their unit as ``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `K`, `M`, `G` and `T` depending on the size +the units are shown in either `KB`, `MB`, `GB` and `TB` depending on the size of each point, the summary values and Y axis are also resized. Example taken from the default configuration of the traffic chart: From 2d0e27b6b80907bad42fcfe72fe7517dff83b4c9 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 6 Jun 2024 18:52:10 +0530 Subject: [PATCH 14/42] [req-changes] Capitalized headings --- docs/developer/extending.rst | 28 +++++++-------- docs/developer/index.rst | 4 +-- docs/developer/installation.rst | 12 +++---- docs/developer/utils.rst | 8 ++--- docs/user/adaptive-size-charts.rst | 30 ---------------- docs/user/adding-checks-and-alertsettings.rst | 2 +- docs/user/checks.rst | 2 +- docs/user/configuring-iperf3-check.rst | 34 +++++++++---------- docs/user/metrics.rst | 2 +- docs/user/quickstart.rst | 12 +++---- docs/user/rest-api.rst | 22 ++++++------ docs/user/settings.rst | 2 +- docs/user/wifi-sessions.rst | 2 +- 13 files changed, 65 insertions(+), 95 deletions(-) delete mode 100644 docs/user/adaptive-size-charts.rst diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 55991027..ac875161 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -26,7 +26,7 @@ code to get a basic derivative of *openwisp-monitoring* working. suggest to start with it since the beginning, because migrating your data from the default module to your extended version may be time consuming. -1. Initialize your custom module +1. Initialize your Custom Module -------------------------------- The first thing you need to do in order to extend any @@ -132,7 +132,7 @@ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your } ] -6. Inherit the AppConfig class +6. Inherit the AppConfig Class ------------------------------ Please refer to the following files in the sample app of the test project: @@ -154,7 +154,7 @@ For more information regarding the concept of ``AppConfig`` please refer to the `"Applications" section in the django documentation `_. -7. Create your custom models +7. Create your Custom Models ---------------------------- To extend ``check`` app, refer to `sample_check models.py file @@ -175,7 +175,7 @@ models.py file - For doubts regarding proxy models please refer to `proxy models `_. -8. Add swapper configurations +8. Add Swapper Configurations ----------------------------- Add the following to your ``settings.py``: @@ -200,7 +200,7 @@ Add the following to your ``settings.py``: Substitute ```` with your actual django app name (also known as ``app_label``). -9. Create database migrations +9. Create Database Migrations ----------------------------- Create and apply database migrations: @@ -214,7 +214,7 @@ For more information, refer to the `"Migrations" section in the django documentation `_. -10. Create your custom admin +10. Create your Custom Admin ---------------------------- To extend ``check`` app, refer to `sample_check admin.py file @@ -237,7 +237,7 @@ below. django documentation `_. -1. Monkey patching +1. Monkey Patching ~~~~~~~~~~~~~~~~~~ If the changes you need to add are relatively small, you can resort to @@ -276,7 +276,7 @@ Similarly for ``monitoring`` app, you can do it as: AlertSettingsAdmin.list_display.insert(1, "my_custom_field") AlertSettingsAdmin.ordering = ["-my_custom_field"] -2. Inheriting admin classes +2. Inheriting Admin Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you need to introduce significant changes and/or you don't want to @@ -363,7 +363,7 @@ For ``monitoring`` app, # add your changes here pass -11. Create root URL configuration +11. Create Root URL Configuration --------------------------------- Please refer to the `urls.py @@ -374,8 +374,8 @@ For more information about URL configuration in django, please refer to the `"URL dispatcher" section in the django documentation `_. -12. Create celery.py --------------------- +12. Create ``celery.py`` +------------------------ Please refer to the `celery.py `_ @@ -395,7 +395,7 @@ Add the following in your settings.py to import celery tasks from CELERY_IMPORTS = ("openwisp_monitoring.device.tasks",) -14. Create the custom command ``run_checks`` +14. Create the Custom Command ``run_checks`` -------------------------------------------- Please refer to the `run_checks.py @@ -407,7 +407,7 @@ django, please refer to the `"Writing custom django-admin commands" section in the django documentation `_. -15. Import the automated tests +15. Import the Automated Tests ------------------------------ When developing a custom application based on this module, it's a good @@ -431,7 +431,7 @@ For, extending ``monitoring`` app see the `tests of sample_monitoring app `_ to find out how to do this. -Other base classes that can be inherited and extended +Other Base Classes that can be Inherited and Extended ----------------------------------------------------- **The following steps are not required and are intended for more advanced diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 99915fc9..f632316f 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -1,5 +1,5 @@ -Developer Docs -============== +Developer Docs Index +==================== .. include:: ../partials/developer-docs.rst diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index a0975495..91738c67 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,5 +1,5 @@ -Installation instructions -========================= +Developer Installation Instructions +=================================== .. include:: ../partials/developer-docs.rst @@ -11,7 +11,7 @@ Dependencies - fping - OpenSSL -Installing for development +Installing for Development -------------------------- Install the system dependencies: @@ -108,10 +108,10 @@ Run quality assurance tests with: ./run-qa-checks -Alternative sources +Alternative Sources ------------------- -Pypi +PyPI ~~~~ To install the latest stable version from pypi: @@ -135,7 +135,7 @@ Alternatively you can use the git protocol: pip install -e git+git://github.com/openwisp/openwisp-monitoring#egg=openwisp_monitoring -Install and run on docker +Install and Run on Docker ------------------------- .. warning:: diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index b528432c..7ab0c33d 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -247,7 +247,7 @@ If you don't need to register a new chart but need to change a specific key of an existing chart configuration, you can use :ref:`OPENWISP_MONITORING_CHARTS `. -Adaptive size charts +Adaptive Size Charts ++++++++++++++++++++ .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png @@ -256,8 +256,8 @@ Adaptive size charts When configuring charts, it is possible to flag their unit as ``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `KB`, `MB`, `GB` and `TB` depending on the size -of each point, the summary values and Y axis are also resized. +the units are shown in either `KB`, `MB`, `GB` and `TB` depending on the +size of each point, the summary values and Y axis are also resized. Example taken from the default configuration of the traffic chart: @@ -431,7 +431,7 @@ to the timeseries database. This signal is emitted for every metric after the write operation is successfully executed in the background. -Management commands +Management Commands ------------------- .. _run_checks: diff --git a/docs/user/adaptive-size-charts.rst b/docs/user/adaptive-size-charts.rst deleted file mode 100644 index caffba4a..00000000 --- a/docs/user/adaptive-size-charts.rst +++ /dev/null @@ -1,30 +0,0 @@ -Adaptive size charts -==================== - -.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/adaptive-chart.png - :align: center - -When configuring charts, it is possible to flag their unit as -``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `K`, `M`, `G` and `T` depending on the size -of each point, the summary values and Y axis are also resized. - -Example taken from the default configuration of the traffic chart: - -.. code-block:: python - - OPENWISP_MONITORING_CHARTS = { - "traffic": { - # other configurations for this chart - # traffic measured in 'B' (bytes) - # unit B, KB, MB, GB, TB - "unit": "adaptive_prefix+B", - }, - "bandwidth": { - # other configurations for this chart - # adaptive unit for bandwidth related charts - # bandwidth measured in 'bps'(bits/sec) - # unit bps, Kbps, Mbps, Gbps, Tbps - "unit": "adaptive_prefix+bps", - }, - } diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst index 9517769b..b2af4bdf 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -1,6 +1,6 @@ .. _adding_checks_and_alertsettings: -Adding Checks and Alert settings from the device page +Adding Checks and Alert Settings from the Device Page ===================================================== We can add checks and define alert settings directly from the **device diff --git a/docs/user/checks.rst b/docs/user/checks.rst index d96a85ee..976e736a 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -17,7 +17,7 @@ You can change the default values used for ping checks using .. _config_applied_check: -Configuration applied +Configuration Applied --------------------- This check ensures that the `openwisp-config agent diff --git a/docs/user/configuring-iperf3-check.rst b/docs/user/configuring-iperf3-check.rst index 0643671a..62405a33 100644 --- a/docs/user/configuring-iperf3-check.rst +++ b/docs/user/configuring-iperf3-check.rst @@ -1,7 +1,7 @@ Configuring Iperf3 Check ======================== -1. Make sure iperf3 is installed on the device +1. Make Sure Iperf3 is Installed on the Device ---------------------------------------------- Register your device to OpenWISP and make sure the `iperf3 openwrt package @@ -13,12 +13,12 @@ device, eg: opkg install iperf3 # if using without authentication opkg install iperf3-ssl # if using with authentication (read below for more info) -2. Ensure SSH access from OpenWISP is enabled on your devices +2. Ensure SSH Access from OpenWISP is Enabled on your Devices ------------------------------------------------------------- Follow the steps in :doc:`"Configuring Push Operations" -` section of the documentation to allow SSH -access to you device from OpenWISP. +` section of the documentation to allow +SSH access to you device from OpenWISP. .. important:: @@ -30,7 +30,7 @@ access to you device from OpenWISP. :alt: Enable ssh access from openwisp to device :align: center -3. Set up and configure Iperf3 server settings +3. Set Up and Configure Iperf3 Server Settings ---------------------------------------------- After having deployed your Iperf3 servers, you need to configure the @@ -104,7 +104,7 @@ Once the changes are saved, you will need to restart all the processes. We recommended to configure this check to run in non peak traffic times to not interfere with standard traffic. -4. Run the check +4. Run the Check ---------------- This should happen automatically if you have celery-beat correctly @@ -119,7 +119,7 @@ After that, you should see the iperf3 network measurements charts. .. _iperf3_check_parameters: -Iperf3 check parameters +Iperf3 Check Parameters ----------------------- Currently, iperf3 check supports the following parameters: @@ -136,7 +136,7 @@ Currently, iperf3 check supports the following parameters: .. _iperf3_client_parameters: -Iperf3 client options +Iperf3 Client Options ~~~~~~~~~~~~~~~~~~~~~ =================== ======== ========================================== @@ -158,7 +158,7 @@ Iperf3 client options .. _iperf3_client_tcp_options: -Iperf3 client's TCP options +Iperf3 Client's TCP Options +++++++++++++++++++++++++++ ============== ======== ================= @@ -169,7 +169,7 @@ Iperf3 client's TCP options .. _iperf3_client_udp_options: -Iperf3 client's UDP options +Iperf3 Client's UDP Options +++++++++++++++++++++++++++ ============== ======== ================= @@ -184,7 +184,7 @@ configuration example `. Visit the `official documentation `_ to learn more about the iperf3 parameters. -Iperf3 authentication +Iperf3 Authentication --------------------- By default iperf3 check runs without any kind of **authentication**, in @@ -192,10 +192,10 @@ this section we will explain how to configure **RSA authentication** between the **client** and the **server** to restrict connections to authenticated clients. -Server side +Server Side ~~~~~~~~~~~ -1. Generate RSA keypair +1. Generate RSA Keypair +++++++++++++++++++++++ .. code-block:: shell @@ -210,7 +210,7 @@ in :ref:`openwisp_monitoring_iperf3_check_config` and the private key will be contained in the file ``private_key.pem`` which will be used with **--rsa-private-key-path** command option when starting the iperf3 server. -2. Create user credentials +2. Create User Credentials ++++++++++++++++++++++++++ .. code-block:: shell @@ -227,14 +227,14 @@ Add the above hash with username in ``credentials.csv`` # file format: username,sha256 iperfuser,ee17a7f98cc87a6424fb52682396b2b6c058e9ab70e946188faa0714905771d7 -3. Now start the iperf3 server with auth options +3. Now Start the Iperf3 Server with Auth Options ++++++++++++++++++++++++++++++++++++++++++++++++ .. code-block:: shell iperf3 -s --rsa-private-key-path ./private_key.pem --authorized-users-path ./credentials.csv -Client side (OpenWrt device) +Client Side (OpenWrt Device) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. Install iperf3-ssl @@ -257,7 +257,7 @@ You may also check your installed **iperf3 openwrt package** features: .. _configure_iperf3_check_auth_parameters: -2. Configure iperf3 check auth parameters +2. Configure Iperf3 Check Auth Parameters +++++++++++++++++++++++++++++++++++++++++ Now, add the following iperf3 authentication parameters to diff --git a/docs/user/metrics.rst b/docs/user/metrics.rst index 7d1cf473..7daa1d52 100644 --- a/docs/user/metrics.rst +++ b/docs/user/metrics.rst @@ -197,7 +197,7 @@ Mobile Signal Quality .. _mobile_access_technology_in_use: -Mobile Access Technology in use +Mobile Access Technology in Use ------------------------------- ================== =============== diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 055fe128..3739a99f 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,7 +1,7 @@ Quickstart Guide ================ -Install monitoring packages on the device +Install Monitoring Packages on the Device ----------------------------------------- `Install the openwrt-openwisp-monitoring packages @@ -14,15 +14,15 @@ like interface traffic, WiFi clients, CPU load, memory usage, etc. .. _openwisp_reach_devices: -Make sure OpenWISP can reach your devices +Make Sure OpenWISP can Reach your Devices ----------------------------------------- In order to perform :doc:`active checks <./checks>` and other actions like :doc:`triggering the push of configuration changes `, :doc:`executing shell commands `, or :doc:`performing firmware upgrades -`, **the OpenWISP server needs to be able to reach the -network devices**. +`, **the OpenWISP server needs to be able to +reach the network devices**. There are mainly two deployment scenarios for OpenWISP: @@ -33,7 +33,7 @@ There are mainly two deployment scenarios for OpenWISP: in the same Layer 2 network (that is, in the same LAN) where the devices are located. **in this case a management tunnel is NOT needed** -1. Public internet deployment +1. Public Internet Deployment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is the most common scenario: @@ -90,7 +90,7 @@ In this scenario, the following requirements are needed: # ... other configuration directives ... option management_interface 'tun0' -2. LAN deployment +2. LAN Deployment ~~~~~~~~~~~~~~~~~ When the OpenWISP server and the network devices are deployed in the same diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 580e0e1d..de620ef5 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -3,7 +3,7 @@ Rest API Reference .. _monitoring_live_documentation: -Live documentation +Live Documentation ------------------ .. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-doc.png @@ -14,7 +14,7 @@ A general live API documentation (following the OpenAPI specification) at .. _monitoring_browsable_web_interface: -Browsable web interface +Browsable Web Interface ----------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/api-ui-1.png @@ -31,7 +31,7 @@ makes it even easier to find out the details of each endpoint. .. _monitoring_rest_endpoints: -List of endpoints +List of Endpoints ----------------- Since the detailed explanation is contained in the :ref:`Live @@ -40,7 +40,7 @@ web page ` of each point, here we'll provide just a list of the available endpoints, for further information please open the URL of the endpoint in your browser. -Retrieve general monitoring charts +Retrieve General Monitoring Charts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -76,7 +76,7 @@ multi-tenancy and allows filtering monitoring data by The ``start`` and ``end`` parameters should be in the format ``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. -Retrieve device charts and device status data +Retrieve Device Charts and Device Status Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -119,7 +119,7 @@ The format used for Device Status is inspired by `NetJSON DeviceMonitoring The ``start`` and ``end`` parameters must be in the format ``YYYY-MM-DD H:M:S``, otherwise 400 Bad Response will be returned. -List device monitoring information +List Device Monitoring Information ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -160,7 +160,7 @@ slug, you can use the ``organization_slug``. GET /api/v1/monitoring/device/?organization_slug={organization_slug} -Collect device metrics and status +Collect Device Metrics and Status ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -191,7 +191,7 @@ sending of data by the OpenWISP Monitoring Agent `_, this feature allows sending data collected while the device is offline. -List nearby devices +List Nearby Devices ~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -222,7 +222,7 @@ Here's a few examples: GET /api/v1/monitoring/device/{pk}/nearby-devices/?model={model1,model2} GET /api/v1/monitoring/device/{pk}/nearby-devices/?distance__lte={distance} -List wifi session +List WiFi Session ~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -263,7 +263,7 @@ For example: GET /api/v1/monitoring/wifi-session/?stop_time__lt={stop_time} GET /api/v1/monitoring/wifi-session/?stop_time__lte={stop_time} -Get wifi session +Get WiFi Session ~~~~~~~~~~~~~~~~ .. code-block:: text @@ -273,7 +273,7 @@ Get wifi session Pagination ~~~~~~~~~~ -Wifi session endpoint support the ``page_size`` parameter that allows +WiFi session endpoint support the ``page_size`` parameter that allows paginating the results in conjunction with the page parameter. .. code-block:: text diff --git a/docs/user/settings.rst b/docs/user/settings.rst index ee469956..6be6ca80 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -48,7 +48,7 @@ The following table describes all keys available in .. _timeseries_backend_options: -Timeseries database options +Timeseries Database Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ============== ===================================================== diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 6988d42e..1184804f 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -28,7 +28,7 @@ device's change admin under the "WiFi Sessions" tab. :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/device-wifi-session-inline.png :align: center -Scheduled deletion of WiFi sessions +Scheduled Deletion of WiFi Sessions ----------------------------------- OpenWISP Monitoring provides a celery task to automatically delete WiFi From bfedba64911c87386ef75b063b1e98ab2bee6c59 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 6 Jun 2024 19:37:40 +0530 Subject: [PATCH 15/42] [req-changes] Improved docs --- README.rst | 55 +----------------- docs/developer/utils.rst | 68 +++++++---------------- docs/index.rst | 1 - docs/user/dashboard-monitoring-charts.rst | 2 + docs/user/intro.rst | 20 ++++--- docs/user/metrics.rst | 4 +- docs/user/quickstart.rst | 2 + docs/user/rest-api.rst | 6 +- docs/user/settings.rst | 32 +++++++++++ 9 files changed, 75 insertions(+), 115 deletions(-) diff --git a/README.rst b/README.rst index 8480d43c..da63f5e4 100644 --- a/README.rst +++ b/README.rst @@ -78,60 +78,9 @@ see the `OpenWISP Architecture Overview .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png :align: center -Available Features ------------------- - -- Collection of monitoring information in a timeseries database (currently - only influxdb is supported) -- Allows to browse alerts easily from the user interface with one click -- Collects and displays `device status <#device-status>`_ information like - uptime, RAM status, CPU load averages, Interface properties and - addresses, WiFi interface status and associated clients, Neighbors - information, DHCP Leases, Disk/Flash status -- Monitoring charts for `uptime <#ping>`_, `packet loss <#ping>`_, `round - trip time (latency) <#ping>`_, `associated wifi clients - <#wifi-clients>`_, `interface traffic <#traffic>`_, `RAM usage - <#memory-usage>`_, `CPU load <#cpu-load>`_, `flash/disk usage - <#disk-usage>`_, mobile signal (LTE/UMTS/GSM `signal strength - <#mobile-signal-strength>`_, `signal quality <#mobile-signal-quality>`_, - `access technology in use <#mobile-access-technology-in-use>`_), - `bandwidth <#iperf3>`_, `transferred data <#iperf3>`_, `restransmits - <#iperf3>`_, `jitter <#iperf3>`_, `datagram <#iperf3>`_, `datagram loss - <#iperf3>`_ -- 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 the last 1 day, 3 days, 7 days, - 30 days, and 365 days -- Configurable alerts -- CSV Export of monitoring data -- An overview of the status of the network is shown in the admin - dashboard, a chart shows the percentages of devices which are online, - offline or having issues; there are also `two timeseries charts which - show the total unique WiFI clients and the traffic flowing to the - network `_, a geographic map is also - available for those who use the geographic features of OpenWISP -- Possibility to configure additional `Metrics - <#openwisp_monitoring_metrics>`_ and `Charts - <#openwisp_monitoring_charts>`_ -- Extensible active check system: it's possible to write additional checks - that are run periodically using python classes -- Extensible metrics and charts: it's possible to define new metrics and - new charts -- API to retrieve the chart metrics and status information of each device - based on `NetJSON DeviceMonitoring - `_ -- `Iperf3 check <#iperf3-1>`_ that provides network performance - measurements such as maximum achievable bandwidth, jitter, datagram loss - etc of the openwrt device using `iperf3 utility `_ ----- - -.. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 - ----- +Refer to `Features: Monitoring ` +of OpenWISP documentation for a complete overview of features. Contributing ------------ diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 7ab0c33d..d3ba7461 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -247,38 +247,6 @@ If you don't need to register a new chart but need to change a specific key of an existing chart configuration, you can use :ref:`OPENWISP_MONITORING_CHARTS `. -Adaptive Size Charts -++++++++++++++++++++ - -.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png - :align: center - -When configuring charts, it is possible to flag their unit as -``adaptive_prefix``, this allows to make the charts more readable because -the units are shown in either `KB`, `MB`, `GB` and `TB` depending on the -size of each point, the summary values and Y axis are also resized. - -Example taken from the default configuration of the traffic chart: - -.. code-block:: python - - OPENWISP_MONITORING_CHARTS = { - "traffic": { - # other configurations for this chart - # traffic measured in 'B' (bytes) - # unit B, KB, MB, GB, TB - "unit": "adaptive_prefix+B", - }, - "bandwidth": { - # other configurations for this chart - # adaptive unit for bandwidth related charts - # bandwidth measured in 'bps'(bits/sec) - # unit bps, Kbps, Mbps, Gbps, Tbps - "unit": "adaptive_prefix+bps", - }, - } - ``unregister_chart`` ~~~~~~~~~~~~~~~~~~~~ @@ -342,7 +310,8 @@ Signals ``device_metrics_received`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_monitoring.device.signals.device_metrics_received`` +**Full Python path**: +``openwisp_monitoring.device.signals.device_metrics_received`` **Arguments**: @@ -362,7 +331,8 @@ not sent if the response was not successful. ``health_status_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_monitoring.device.signals.health_status_changed`` +**Full Python path**: +``openwisp_monitoring.device.signals.health_status_changed`` **Arguments**: @@ -377,7 +347,8 @@ object gets updated. ``threshold_crossed`` ~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_monitoring.monitoring.signals.threshold_crossed`` +**Full Python path**: +``openwisp_monitoring.monitoring.signals.threshold_crossed`` **Arguments**: @@ -400,7 +371,8 @@ alert settings is crossed. ``pre_metric_write`` ~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_monitoring.monitoring.signals.pre_metric_write`` +**Full Python path**: +``openwisp_monitoring.monitoring.signals.pre_metric_write`` **Arguments**: @@ -417,7 +389,8 @@ to the timeseries database. ``post_metric_write`` ~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_monitoring.monitoring.signals.post_metric_write`` +**Full Python path**: +``openwisp_monitoring.monitoring.signals.post_metric_write`` **Arguments**: @@ -439,8 +412,8 @@ Management Commands ``run_checks`` ~~~~~~~~~~~~~~ -This command will execute all the `available checks `_ for all the -devices. By default checks are run periodically by *celery beat*. +This command will execute all the :ref:`available checks ` for all +the devices. By default checks are run periodically by *celery beat*. Example usage: @@ -467,29 +440,30 @@ Exceptions ``TimeseriesWriteException`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` +**Full Python path**: +``openwisp_monitoring.db.exceptions.TimeseriesWriteException`` If there is any failure due while writing data in timeseries database, -this exception shall be raised with a helpful error message explaining the +this exception will be raised with a helpful error message explaining the cause of the failure. This exception will normally be caught and the failed write task will be retried in the background so that there is no loss of data if failures occur due to overload of Timeseries server. You can read more about this retry mechanism at -`OPENWISP_MONITORING_WRITE_RETRY_OPTIONS -<#openwisp-monitoring-write-retry-options>`_. +:ref:`OPENWISP_MONITORING_WRITE_RETRY_OPTIONS +`. ``InvalidMetricConfigException`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: +**Full Python path**: ``openwisp_monitoring.monitoring.exceptions.InvalidMetricConfigException`` -This exception shall be raised if the metric configuration is broken. +This exception will be raised if the metric configuration is broken. ``InvalidChartConfigException`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: +**Full Python path**: ``openwisp_monitoring.monitoring.exceptions.InvalidChartConfigException`` -This exception shall be raised if the chart configuration is broken. +This exception will be raised if the chart configuration is broken. diff --git a/docs/index.rst b/docs/index.rst index 1d1550a3..bff1b696 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,7 +21,6 @@ Refer to :doc:`user/intro` for a complete overview of features. ./user/configuring-iperf3-check.rst ./user/dashboard-monitoring-charts.rst ./user/wifi-sessions.rst - ./user/default-alerts-and-notifications.rst ./user/rest-api.rst ./user/settings.rst diff --git a/docs/user/dashboard-monitoring-charts.rst b/docs/user/dashboard-monitoring-charts.rst index e2a6a86a..6fa17487 100644 --- a/docs/user/dashboard-monitoring-charts.rst +++ b/docs/user/dashboard-monitoring-charts.rst @@ -1,3 +1,5 @@ +.. _dashboard_monitoring_charts: + Dashboard Monitoring Charts =========================== diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 02ae4e44..5dd4a23a 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -3,6 +3,12 @@ Monitoring: Features OpenWISP provides the following monitoring capabilities: +- An overview of the status of the network is shown in the admin + dashboard, a chart shows the percentages of devices which are online, + offline or having issues; there are also :doc:`two timeseries charts + which show the total unique WiFI clients and the traffic flowing to the + network `, a geographic map is also + available for those who use the geographic features of OpenWISP - Collection of monitoring information in a timeseries database (currently only influxdb is supported) - Allows to browse alerts easily from the user interface with one click @@ -28,18 +34,14 @@ OpenWISP provides the following monitoring capabilities: 30 days, and 365 days - Configurable alerts - CSV Export of monitoring data -- An overview of the status of the network is shown in the admin - dashboard, a chart shows the percentages of devices which are online, - offline or having issues; there are also :doc:`two timeseries charts - which show the total unique WiFI clients and the traffic flowing to the - network `, a geographic map is also - available for those who use the geographic features of OpenWISP - Possibility to configure additional :ref:`Metrics ` and :ref:`Charts ` -- Extensible active check system: it's possible to write additional checks - that are run periodically using python classes -- Extensible metrics and charts: it's possible to define new metrics and +- :ref:`Extensible active check system `: + it's possible to write additional checks that are run periodically using + python classes +- Extensible :ref:`metrics ` and :ref:`charts + `: it's possible to define new metrics and new charts - API to retrieve the chart metrics and status information of each device based on `NetJSON DeviceMonitoring diff --git a/docs/user/metrics.rst b/docs/user/metrics.rst index 7daa1d52..64aee694 100644 --- a/docs/user/metrics.rst +++ b/docs/user/metrics.rst @@ -291,8 +291,8 @@ Monitoring can be divided in two categories: continuously sends network requests to the devices and store the results; 2. **metrics collected passively by OpenWISP**: these metrics are sent by - the `openwrt-openwisp-monitoring agent - <#install-monitoring-packages-on-the-device>`_ installed on the network + the :ref:`openwrt-openwisp-monitoring agent + ` installed on the network devices and are collected by OpenWISP via its REST API. The :doc:`checks` section of the documentation lists the currently diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 3739a99f..4c892758 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,6 +1,8 @@ Quickstart Guide ================ +.. _install_monitoring_packages_on_device: + Install Monitoring Packages on the Device ----------------------------------------- diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index de620ef5..2e8c32ed 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -34,9 +34,9 @@ makes it even easier to find out the details of each endpoint. List of Endpoints ----------------- -Since the detailed explanation is contained in the :ref:`Live -documentation ` and in the :ref:`Browsable -web page ` of each point, here we'll +Since the detailed explanation is contained in the +:ref:`monitoring_live_documentation` and in the +:ref:`monitoring_browsable_web_interface` of each point, here we'll provide just a list of the available endpoints, for further information please open the URL of the endpoint in your browser. diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 6be6ca80..b22af1ea 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -745,6 +745,38 @@ do it: "traffic": {"colors": ["#000000", "#cccccc", "#111111"]} } +Adaptive Size Charts +~~~~~~~~~~~~~~~~~~~~ + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/adaptive-chart.png + :align: center + +When configuring charts, it is possible to flag their unit as +``adaptive_prefix``, this allows to make the charts more readable because +the units are shown in either `KB`, `MB`, `GB` and `TB` depending on the +size of each point, the summary values and Y axis are also resized. + +Example taken from the default configuration of the traffic chart: + +.. code-block:: python + + OPENWISP_MONITORING_CHARTS = { + "traffic": { + # other configurations for this chart + # traffic measured in 'B' (bytes) + # unit B, KB, MB, GB, TB + "unit": "adaptive_prefix+B", + }, + "bandwidth": { + # other configurations for this chart + # adaptive unit for bandwidth related charts + # bandwidth measured in 'bps'(bits/sec) + # unit bps, Kbps, Mbps, Gbps, Tbps + "unit": "adaptive_prefix+bps", + }, + } + .. _openwisp_monitoring_default_chart_time: ``OPENWISP_MONITORING_DEFAULT_CHART_TIME`` From 141795b5ce5a3592cab32682a581de7f716ddff1 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 6 Jun 2024 22:31:35 +0530 Subject: [PATCH 16/42] [skip ci] Fixed README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index da63f5e4..09b481b9 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ see the `OpenWISP Architecture Overview :align: center -Refer to `Features: Monitoring ` +Refer to `Monitoring: Features ` of OpenWISP documentation for a complete overview of features. Contributing From 6baf7993a0a016d029160cd09df291d4316f6208 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 7 Jun 2024 00:32:13 +0530 Subject: [PATCH 17/42] [docs] incorporated copy writer's changes --- docs/user/quickstart.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 4c892758..664d3d19 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -33,7 +33,7 @@ There are mainly two deployment scenarios for OpenWISP: case a management tunnel is needed** 2. the OpenWISP server is deployed on a computer/server which is located in the same Layer 2 network (that is, in the same LAN) where the - devices are located. **in this case a management tunnel is NOT needed** + devices are located. **In this case a management tunnel is NOT needed** 1. Public Internet Deployment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -47,7 +47,7 @@ This is the most common scenario: locations (different cities, different regions, different countries) In this scenario, the OpenWISP application will not be able to reach the -devices **unless a management tunnel** is used, for that reason having a +devices unless a management tunnel is used, for that reason having a management VPN like OpenVPN, Wireguard, ZeroTier or any other tunneling solution is paramount, not only to allow OpenWISP to work properly, but also to be able to perform debugging and troubleshooting when needed. @@ -55,7 +55,7 @@ also to be able to perform debugging and troubleshooting when needed. In this scenario, the following requirements are needed: - a VPN server must be installed in a way that the OpenWISP server can - reach the VPN peers, for more information on how to do this via OpenWISP + reach the VPN peers. For more information on how to do this via OpenWISP please refer to the following sections: - :doc:`OpenVPN tunnel automation ` @@ -68,8 +68,8 @@ In this scenario, the following requirements are needed: If the OpenWISP server is connected to a network infrastructure which allows it to reach the devices via pre-existing tunneling or Intranet solutions (eg: MPLS, SD-WAN), then setting up a VPN server is not - needed, as long as there's a dedicated interface on OpenWrt which gets - an IP address assigned to it and which is reachable from the OpenWISP + needed, as long as there's a dedicated interface on OpenWrt which has an + IP address assigned to it and which is reachable from the OpenWISP server. - The devices must be configured to join the management tunnel From 6fb3b4ec7e3cedbda7b1c9107bc65c7da6f914a5 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 7 Jun 2024 16:18:20 +0530 Subject: [PATCH 18/42] [req-changes] Fixed sphinx warnings --- README.rst | 6 ++-- docs/developer/utils.rst | 30 ------------------- docs/index.rst | 4 ++- docs/user/adding-checks-and-alertsettings.rst | 2 -- docs/user/checks.rst | 6 ++-- docs/user/dashboard-monitoring-charts.rst | 2 -- docs/user/intro.rst | 4 +-- docs/user/management-commands.rst | 29 ++++++++++++++++++ 8 files changed, 40 insertions(+), 43 deletions(-) create mode 100644 docs/user/management-commands.rst diff --git a/README.rst b/README.rst index 09b481b9..fb05d9ca 100644 --- a/README.rst +++ b/README.rst @@ -78,9 +78,9 @@ see the `OpenWISP Architecture Overview .. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/dashboard.png :align: center - -Refer to `Monitoring: Features ` -of OpenWISP documentation for a complete overview of features. +For a complete overview of features, refer to the `Monitoring: Features +`_ +section of the OpenWISP documentation. Contributing ------------ diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index d3ba7461..b386b859 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -404,36 +404,6 @@ to the timeseries database. This signal is emitted for every metric after the write operation is successfully executed in the background. -Management Commands -------------------- - -.. _run_checks: - -``run_checks`` -~~~~~~~~~~~~~~ - -This command will execute all the :ref:`available checks ` for all -the devices. By default checks are run periodically by *celery beat*. - -Example usage: - -.. code-block:: shell - - cd tests/ - ./manage.py run_checks - -``migrate_timeseries`` -~~~~~~~~~~~~~~~~~~~~~~ - -This command triggers asynchronous migration of the time-series database. - -Example usage: - -.. code-block:: shell - - cd tests/ - ./manage.py migrate_timeseries - Exceptions ---------- diff --git a/docs/index.rst b/docs/index.rst index bff1b696..3de66389 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,8 @@ Django, designed to be **extensible**, **programmable**, **scalable** and easy to use by end users: once the system is configured, monitoring checks, alerts and metric collection happens automatically. -Refer to :doc:`user/intro` for a complete overview of features. +For a comprehensive overview of features, please refer to the +:doc:`user/intro` page. .. toctree:: :caption: User Docs @@ -23,6 +24,7 @@ Refer to :doc:`user/intro` for a complete overview of features. ./user/wifi-sessions.rst ./user/rest-api.rst ./user/settings.rst + ./user/management-commands.rst .. toctree:: :caption: Developer Docs diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/adding-checks-and-alertsettings.rst index b2af4bdf..a6949517 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/adding-checks-and-alertsettings.rst @@ -1,5 +1,3 @@ -.. _adding_checks_and_alertsettings: - Adding Checks and Alert Settings from the Device Page ===================================================== diff --git a/docs/user/checks.rst b/docs/user/checks.rst index 976e736a..ae8ad405 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -44,7 +44,7 @@ This check is **disabled by default**. You can enable auto creation of this check by setting the :ref:`openwisp_monitoring_auto_iperf3` to ``True``. -You can also :ref:`add the iperf3 check ` +You can also :doc:`add the iperf3 check ` directly from the device page. It also supports tuning of various parameters. You can change the @@ -55,6 +55,6 @@ rsa_publc_key etc) using the .. note:: When setting :ref:`openwisp_monitoring_auto_iperf3` to ``True``, you - may need to update the :ref:`metric configuration ` to enable alerts for the iperf3 + may need to update the :doc:`metric configuration + ` to enable alerts for the iperf3 check. diff --git a/docs/user/dashboard-monitoring-charts.rst b/docs/user/dashboard-monitoring-charts.rst index 6fa17487..e2a6a86a 100644 --- a/docs/user/dashboard-monitoring-charts.rst +++ b/docs/user/dashboard-monitoring-charts.rst @@ -1,5 +1,3 @@ -.. _dashboard_monitoring_charts: - Dashboard Monitoring Charts =========================== diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 5dd4a23a..06e93542 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -7,7 +7,7 @@ OpenWISP provides the following monitoring capabilities: dashboard, a chart shows the percentages of devices which are online, offline or having issues; there are also :doc:`two timeseries charts which show the total unique WiFI clients and the traffic flowing to the - network `, a geographic map is also + network `, a geographic map is also available for those who use the geographic features of OpenWISP - Collection of monitoring information in a timeseries database (currently only influxdb is supported) @@ -37,7 +37,7 @@ OpenWISP provides the following monitoring capabilities: - Possibility to configure additional :ref:`Metrics ` and :ref:`Charts ` -- :ref:`Extensible active check system `: +- :doc:`Extensible active check system `: it's possible to write additional checks that are run periodically using python classes - Extensible :ref:`metrics ` and :ref:`charts diff --git a/docs/user/management-commands.rst b/docs/user/management-commands.rst new file mode 100644 index 00000000..713ffaec --- /dev/null +++ b/docs/user/management-commands.rst @@ -0,0 +1,29 @@ +Management Commands +=================== + +.. _run_checks: + +``run_checks`` +-------------- + +This command will execute all the :doc:`available checks ` for all +the devices. By default checks are run periodically by *celery beat*. + +Example usage: + +.. code-block:: shell + + cd tests/ + ./manage.py run_checks + +``migrate_timeseries`` +---------------------- + +This command triggers asynchronous migration of the time-series database. + +Example usage: + +.. code-block:: shell + + cd tests/ + ./manage.py migrate_timeseries From ea2ee24050607d5535eabe0e93cf09b353b49094 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 7 Jun 2024 16:48:21 +0530 Subject: [PATCH 19/42] [skip ci] Updated README --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index fb05d9ca..d454541b 100644 --- a/README.rst +++ b/README.rst @@ -82,6 +82,12 @@ For a complete overview of features, refer to the `Monitoring: Features `_ section of the OpenWISP documentation. +Documentation +------------- + +- `Developer documentation `_ +- `User documentation `_ + Contributing ------------ From 48f1ae7dca8e166b099cbf8de30d46105f2296a4 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 14 Jun 2024 15:41:16 +0530 Subject: [PATCH 20/42] [skip ci] Fixed references --- docs/developer/utils.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index b386b859..945f5cdb 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -293,13 +293,13 @@ Registering Notification Types You can define your own notification types using ``register_notification_type`` function from OpenWISP Notifications. -For more information, see the relevant :doc:`documentation section about +For more information, see the relevant :ref:`documentation section about registering notification types in the Notifications module -`. +`. Once a new notification type is registered, you have to use the :doc:`"notify" signal provided the Notifications module -` to send notifications +` to send notifications for this type. Signals From 6de98c09beb1d68ee7de031d07a151a8a37116ac Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 14 Jun 2024 22:53:48 +0530 Subject: [PATCH 21/42] [skip ci] Fixed references --- docs/user/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 664d3d19..8f3d0dba 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -23,7 +23,7 @@ In order to perform :doc:`active checks <./checks>` and other actions like :doc:`triggering the push of configuration changes `, :doc:`executing shell commands `, or :doc:`performing firmware upgrades -`, **the OpenWISP server needs to be able to +`, **the OpenWISP server needs to be able to reach the network devices**. There are mainly two deployment scenarios for OpenWISP: From b1dc18b951b4ad040f17894409003bfd4e041910 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 19 Jun 2024 18:12:44 +0530 Subject: [PATCH 22/42] [skip ci] Updated index and fixed references --- README.rst | 3 ++- docs/developer/utils.rst | 4 ++-- docs/index.rst | 9 +++++---- docs/user/quickstart.rst | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index d454541b..bd1725ac 100644 --- a/README.rst +++ b/README.rst @@ -85,7 +85,8 @@ section of the OpenWISP documentation. Documentation ------------- -- `Developer documentation `_ +- `Developer documentation + `_ - `User documentation `_ Contributing diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 945f5cdb..3e870807 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -299,8 +299,8 @@ registering notification types in the Notifications module Once a new notification type is registered, you have to use the :doc:`"notify" signal provided the Notifications module -` to send notifications -for this type. +` to send notifications for +this type. Signals ------- diff --git a/docs/index.rst b/docs/index.rst index 3de66389..ccb0f4be 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,11 @@ Monitoring ========== -OpenWISP Monitoring is a network monitoring system written in Python and -Django, designed to be **extensible**, **programmable**, **scalable** and -easy to use by end users: once the system is configured, monitoring -checks, alerts and metric collection happens automatically. +The OpenWISP Monitoring module leverages the capabilities of Python and +the Django Framework to provide OpenWISP with robust network monitoring +features. Designed to be extensible, programmable, scalable, and +user-friendly, this module automates monitoring checks, alerts, and metric +collection, ensuring efficient and comprehensive network management. For a comprehensive overview of features, please refer to the :doc:`user/intro` page. diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 8f3d0dba..cd566f66 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -23,8 +23,8 @@ In order to perform :doc:`active checks <./checks>` and other actions like :doc:`triggering the push of configuration changes `, :doc:`executing shell commands `, or :doc:`performing firmware upgrades -`, **the OpenWISP server needs to be able to -reach the network devices**. +`, **the OpenWISP server needs to be +able to reach the network devices**. There are mainly two deployment scenarios for OpenWISP: From 861c6d84abc2f0e88cbb61cd15044346ac5593c0 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 19 Jun 2024 19:18:00 +0530 Subject: [PATCH 23/42] [skip ci] Point to Django 4.2 docs --- docs/developer/extending.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index ac875161..00094b4f 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -70,7 +70,7 @@ removed: For more information about how to work with django projects and django apps, please refer to the `"Tutorial: Writing your first Django app" in the django documentation -`_. +`_. 2. Install ``openwisp-monitoring`` ---------------------------------- @@ -152,7 +152,7 @@ Please refer to the following files in the sample app of the test project: For more information regarding the concept of ``AppConfig`` please refer to the `"Applications" section in the django documentation -`_. +`_. 7. Create your Custom Models ---------------------------- @@ -171,9 +171,9 @@ models.py file - For doubts regarding how to use, extend or develop models please refer to the `"Models" section in the django documentation - `_. + `_. - For doubts regarding proxy models please refer to `proxy models - `_. + `_. 8. Add Swapper Configurations ----------------------------- @@ -212,7 +212,7 @@ Create and apply database migrations: For more information, refer to the `"Migrations" section in the django documentation -`_. +`_. 10. Create your Custom Admin ---------------------------- @@ -235,7 +235,7 @@ below. For doubts regarding how the django admin works, or how it can be customized, please refer to `"The django admin site" section in the django documentation - `_. + `_. 1. Monkey Patching ~~~~~~~~~~~~~~~~~~ @@ -372,7 +372,7 @@ file in the test project. For more information about URL configuration in django, please refer to the `"URL dispatcher" section in the django documentation -`_. +`_. 12. Create ``celery.py`` ------------------------ @@ -405,7 +405,7 @@ file in the test project. For more information about the usage of custom management commands in django, please refer to the `"Writing custom django-admin commands" section in the django documentation -`_. +`_. 15. Import the Automated Tests ------------------------------ From 04b7698518dbf3ea1a758b3fe65cd66451c9bed9 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 8 Jul 2024 17:39:44 +0530 Subject: [PATCH 24/42] [skip ci] Updated README --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index bd1725ac..2379b123 100644 --- a/README.rst +++ b/README.rst @@ -43,8 +43,6 @@ Django, designed to be **extensible**, **programmable**, **scalable** and easy to use by end users: once the system is configured, monitoring checks, alerts and metric collection happens automatically. -See the `available features <#available-features>`_. - `OpenWISP `_ is not only an application designed for end users, but can also be used as a framework on which custom network automation solutions can be built on top of its building blocks. From 537d068b69766a448a5c4ed41622e14a2903a024 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 11 Jul 2024 18:32:50 -0400 Subject: [PATCH 25/42] [docs] Added link to github repo --- docs/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index ccb0f4be..712f3484 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,9 @@ Monitoring ========== +**Source code**: `github.com/openwisp/openwisp-monitoring +`_. + The OpenWISP Monitoring module leverages the capabilities of Python and the Django Framework to provide OpenWISP with robust network monitoring features. Designed to be extensible, programmable, scalable, and From 7c6c0c198db01d0eca711ba5434127c1a3b874a4 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 13 Jul 2024 14:25:22 -0400 Subject: [PATCH 26/42] [docs] OpenWRT > OpenWrt [skip ci] --- README.rst | 2 +- docs/user/configuring-iperf3-check.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2379b123..c6648f5e 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ Other popular building blocks that are part of the OpenWISP ecosystem are: - `openwisp-controller `_: network and WiFi controller: provisioning, configuration management, x509 PKI management - and more; works on OpenWRT, but designed to work also on other systems. + and more; works on OpenWrt, but designed to work also on other systems. - `openwisp-network-topology `_: provides way to collect and visualize network topology data from dynamic mesh routing diff --git a/docs/user/configuring-iperf3-check.rst b/docs/user/configuring-iperf3-check.rst index 62405a33..0a2a9f1b 100644 --- a/docs/user/configuring-iperf3-check.rst +++ b/docs/user/configuring-iperf3-check.rst @@ -23,7 +23,7 @@ SSH access to you device from OpenWISP. .. important:: Make sure device connection is enabled & working with right update - strategy i.e. ``OpenWRT SSH``. + strategy i.e. ``OpenWrt SSH``. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/enable-openwrt-ssh.png :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/1.1/enable-openwrt-ssh.png From 5ddbe708d51ce9af2666a8d633b4564e16658b80 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 15 Jul 2024 16:52:15 +0530 Subject: [PATCH 27/42] [skip ci] Fixed references to OpenWISP modules --- README.rst | 10 +++++----- docs/developer/installation.rst | 2 +- docs/user/checks.rst | 8 ++++---- docs/user/quickstart.rst | 15 +++++++-------- docs/user/rest-api.rst | 6 +++--- docs/user/settings.rst | 18 ++++++++---------- docs/user/wifi-sessions.rst | 6 +++--- 7 files changed, 31 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index c6648f5e..3108d1e2 100644 --- a/README.rst +++ b/README.rst @@ -50,23 +50,23 @@ automation solutions can be built on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: - `openwisp-controller - `_: network and WiFi + `_: network and WiFi controller: provisioning, configuration management, x509 PKI management and more; works on OpenWrt, but designed to work also on other systems. - `openwisp-network-topology - `_: provides way + `_: provides way to collect and visualize network topology data from dynamic mesh routing daemons or other network software (eg: OpenVPN); it can be used in conjunction with openwisp-monitoring to get a better idea of the state of the network - `openwisp-firmware-upgrader - `_: automated + `_: automated firmware upgrades (single device or mass network upgrades) -- `openwisp-radius `_: based +- `openwisp-radius `_: based on FreeRADIUS, allows to implement network access authentication systems like 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 (802.11u) -- `openwisp-ipam `_: it allows +- `openwisp-ipam `_: it allows to manage the IP address space of networks **For a more complete overview of the OpenWISP modules and architecture**, diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 91738c67..8390c8d0 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -143,7 +143,7 @@ Install and Run on Docker This Docker image is for development purposes only. For the official OpenWISP Docker images, see: `docker-openwisp - `_. + `_. Build from the Dockerfile: diff --git a/docs/user/checks.rst b/docs/user/checks.rst index ae8ad405..8932f96a 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -20,10 +20,10 @@ You can change the default values used for ping checks using Configuration Applied --------------------- -This check ensures that the `openwisp-config agent -`_ is running and applying -configuration changes in a timely manner. You may choose to disable auto -creation of this check by using the setting +This check ensures that the :doc:`openwisp-config agent +` is running and applying configuration +changes in a timely manner. You may choose to disable auto creation of +this check by using the setting :ref:`openwisp_monitoring_auto_device_config_check`. This check runs periodically, but it is also triggered whenever the diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index cd566f66..e93ab467 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -6,9 +6,8 @@ Quickstart Guide Install Monitoring Packages on the Device ----------------------------------------- -`Install the openwrt-openwisp-monitoring packages -`_ -on your device. +:doc:`Install the openwrt-openwisp-monitoring packages +` on your device. These packages collect and send the monitoring data from the device to OpenWISP Monitoring and are required to collect :doc:`metrics <./metrics>` @@ -75,11 +74,11 @@ In this scenario, the following requirements are needed: - The devices must be configured to join the management tunnel automatically, either via a pre-existing configuration in the firmware or via an :doc:`OpenWISP Template `. -- The `openwisp-config `_ - agent on the devices must be configured to specify the - ``management_interface`` option, the agent will communicate the IP of - the management interface to the OpenWISP Server and OpenWISP will use - the management IP for reaching the device. +- The :doc:`openwisp-config ` agent on the + devices must be configured to specify the ``management_interface`` + option, the agent will communicate the IP of the management interface to + the OpenWISP Server and OpenWISP will use the management IP for reaching + the device. For example, if the *management interface* is named ``tun0``, the openwisp-config configuration should look like the following example: diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 2e8c32ed..4cff9755 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -1,4 +1,4 @@ -Rest API Reference +REST API Reference ================== .. _monitoring_live_documentation: @@ -186,9 +186,9 @@ The format used for Device Status is inspired by `NetJSON DeviceMonitoring If the request is made without passing the ``time`` argument, the server local time will be used. -The ``time`` parameter was added to support `resilient collection and +The ``time`` parameter was added to support :ref:`resilient collection and sending of data by the OpenWISP Monitoring Agent -`_, +`, this feature allows sending data collected while the device is offline. List Nearby Devices diff --git a/docs/user/settings.rst b/docs/user/settings.rst index b22af1ea..aa3c0dac 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -86,10 +86,9 @@ Timeseries Database Options database = "openwisp2" retention-policy = 'short' - If you are using `ansible-openwisp2 - `_ for deploying - OpenWISP, you can set the ``influxdb_udp_mode`` ansible variable to - ``true`` in your playbook, this will make the ansible role + If you are using :doc:`ansible-openwisp2 ` for + deploying OpenWISP, you can set the ``influxdb_udp_mode`` ansible + variable to ``true`` in your playbook, this will make the ansible role automatically configure the InfluxDB UDP listeners. You can refer to the `ansible-ow-influxdb's `_ (a @@ -410,9 +409,8 @@ If you use OpenVPN as the management VPN, you may want to check out a similar integration built in **openwisp-network-topology**: when the status of an OpenVPN link changes (detected by monitoring the status information of OpenVPN), the network topology module will trigger the -monitoring checks. For more information see: `Network Topology Device -Integration -`_ +monitoring checks. For more information see: :doc:`Network Topology Device +Integration `. .. _openwisp_monitoring_mac_vendor_detection: @@ -537,9 +535,9 @@ monitoring status of the devices, this allows to get an overview of the network at glance. This feature is enabled by default and depends on the setting -``OPENWISP_ADMIN_DASHBOARD_ENABLED`` from `openwisp-utils -`__ being set to ``True`` -(which is the default). +``OPENWISP_ADMIN_DASHBOARD_ENABLED`` from :ref:`openwisp-utils +` being set to ``True`` (which is the +default). You can turn this off if you do not use the geographic features of OpenWISP. diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 1184804f..5fa95fe7 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -36,9 +36,9 @@ sessions older than a pre-configured number of days. .. note:: - If you have deployed OpenWISP using `ansible-openwisp2 - `_ or `docker-openwisp - `_, then this feature has + If you have deployed OpenWISP using :doc:`ansible-openwisp2 + ` or :doc:`docker-openwisp + `, then this feature has been already configured for you. Refer to the documentation of your deployment method to know the default value. This section is only for reference for users who wish to customize OpenWISP, or who have From 620cc38963e976fb855a88ef576b955f2c74b437 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 15 Jul 2024 18:05:48 +0530 Subject: [PATCH 28/42] [skip ci] Integrated changes from fb2814b --- docs/developer/utils.rst | 156 +++++++++++++++++++-------------------- docs/user/checks.rst | 8 +- docs/user/intro.rst | 2 +- docs/user/metrics.rst | 8 +- 4 files changed, 86 insertions(+), 88 deletions(-) diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 3e870807..d24aa64e 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -39,100 +39,98 @@ An example usage has been shown below. # Define configuration of your metric metric_config = { - "label": _("Ping"), - "name": "Ping", - "key": "ping", - "field_name": "reachable", - "related_fields": ["loss", "rtt_min", "rtt_max", "rtt_avg"], - "charts": { - "uptime": { - "type": "bar", - "title": _("Uptime"), - "description": _( - "A value of 100% means reachable, 0% means unreachable, values in " - "between 0% and 100% indicate the average reachability in the " - "period observed. Obtained with the fping linux program." + 'label': _('Ping'), + 'name': 'Ping', + 'key': 'ping', + 'field_name': 'reachable', + 'related_fields': ['loss', 'rtt_min', 'rtt_max', 'rtt_avg'], + 'charts': { + 'uptime': { + 'type': 'bar', + 'title': _('Ping Success Rate'), + 'description': _( + 'A value of 100% means reachable, 0% means unreachable, values in ' + 'between 0% and 100% indicate the average reachability in the ' + 'period observed. Obtained with the fping linux program.' ), - "summary_labels": [_("Average uptime")], - "unit": "%", - "order": 200, - "colorscale": { - "max": 100, - "min": 0, - "label": _("Reachable"), - "scale": [ - [ - [0, "#c13000"], - [0.1, "cb7222"], - [0.5, "#deed0e"], - [0.9, "#7db201"], - [1, "#498b26"], - ], + 'summary_labels': [_('Average Ping Success Rate')], + 'unit': '%', + 'order': 200, + 'colorscale': { + 'max': 100, + 'min': 0, + 'label': _('Rate'), + 'scale': [ + [[0, '#c13000'], + [0.1,'cb7222'], + [0.5,'#deed0e'], + [0.9, '#7db201'], + [1, '#498b26']], ], - "map": [ - [100, "#498b26", _("Reachable")], - [90, "#7db201", _("Mostly Reachable")], - [50, "#deed0e", _("Partly Reachable")], - [10, "#cb7222", _("Mostly Unreachable")], - [None, "#c13000", _("Unreachable")], + 'map': [ + [100, '#498b26', _('Flawless')], + [90, '#7db201', _('Mostly Reachable')], + [50, '#deed0e', _('Partly Reachable')], + [10, '#cb7222', _('Mostly Unreachable')], + [None, '#c13000', _('Unreachable')], ], - "fixed_value": 100, + 'fixed_value': 100, }, - "query": chart_query["uptime"], + 'query': chart_query['uptime'], }, - "packet_loss": { - "type": "bar", - "title": _("Packet loss"), - "description": _( - "Indicates the percentage of lost packets observed in ICMP probes. " - "Obtained with the fping linux program." + 'packet_loss': { + 'type': 'bar', + 'title': _('Packet loss'), + 'description': _( + 'Indicates the percentage of lost packets observed in ICMP probes. ' + 'Obtained with the fping linux program.' ), - "summary_labels": [_("Average packet loss")], - "unit": "%", - "colors": "#d62728", - "order": 210, - "query": chart_query["packet_loss"], + 'summary_labels': [_('Average packet loss')], + 'unit': '%', + 'colors': '#d62728', + 'order': 210, + 'query': chart_query['packet_loss'], }, - "rtt": { - "type": "scatter", - "title": _("Round Trip Time"), - "description": _( - "Round trip time observed in ICMP probes, measuered in milliseconds." + 'rtt': { + 'type': 'scatter', + 'title': _('Round Trip Time'), + 'description': _( + 'Round trip time observed in ICMP probes, measuered in milliseconds.' ), - "summary_labels": [ - _("Average RTT"), - _("Average Max RTT"), - _("Average Min RTT"), + 'summary_labels': [ + _('Average RTT'), + _('Average Max RTT'), + _('Average Min RTT'), ], - "unit": _(" ms"), - "order": 220, - "query": chart_query["rtt"], + 'unit': _(' ms'), + 'order': 220, + 'query': chart_query['rtt'], }, }, - "alert_settings": {"operator": "<", "threshold": 1, "tolerance": 0}, - "notification": { - "problem": { - "verbose_name": "Ping PROBLEM", - "verb": "cannot be reached anymore", - "level": "warning", - "email_subject": _( - "[{site.name}] {notification.target} is not reachable" + 'alert_settings': {'operator': '<', 'threshold': 1, 'tolerance': 0}, + 'notification': { + 'problem': { + 'verbose_name': 'Ping PROBLEM', + 'verb': 'cannot be reached anymore', + 'level': 'warning', + 'email_subject': _( + '[{site.name}] {notification.target} is not reachable' ), - "message": _( - "The device [{notification.target}] {notification.verb} anymore by our ping " - "messages." + 'message': _( + 'The device [{notification.target}] {notification.verb} anymore by our ping ' + 'messages.' ), }, - "recovery": { - "verbose_name": "Ping RECOVERY", - "verb": "has become reachable", - "level": "info", - "email_subject": _( - "[{site.name}] {notification.target} is reachable again" + 'recovery': { + 'verbose_name': 'Ping RECOVERY', + 'verb': 'has become reachable', + 'level': 'info', + 'email_subject': _( + '[{site.name}] {notification.target} is reachable again' ), - "message": _( - "The device [{notification.target}] {notification.verb} again by our ping " - "messages." + 'message': _( + 'The device [{notification.target}] {notification.verb} again by our ping ' + 'messages.' ), }, }, diff --git a/docs/user/checks.rst b/docs/user/checks.rst index 8932f96a..9bc41cb7 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -6,10 +6,10 @@ Checks Ping ---- -This check returns information on device ``uptime`` and ``RTT (Round trip -time)``. The Charts ``uptime``, ``packet loss`` and ``rtt`` are created. -The ``fping`` command is used to collect these metrics. You may choose to -disable auto creation of this check by setting +This check returns information on Ping Success Rate and RTT (Round trip time). +It creates charts like Ping Success Rate, Packet Loss and RTT. +These metrics are collected using the ``fping`` Linux program. +You may choose to disable auto creation of this check by setting :ref:`openwisp_monitoring_auto_ping` to ``False``. You can change the default values used for ping checks using diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 06e93542..439ac9b5 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -16,7 +16,7 @@ OpenWISP provides the following monitoring capabilities: like uptime, RAM status, CPU load averages, Interface properties and addresses, WiFi interface status and associated clients, Neighbors information, DHCP Leases, Disk/Flash status -- Monitoring charts for :ref:`uptime `, :ref:`packet loss +- Monitoring charts for :ref:`ping success rate `, :ref:`packet loss `, :ref:`round trip time (latency) `, :ref:`associated wifi clients `, :ref:`interface traffic `, :ref:`RAM usage `, :ref:`CPU load `, diff --git a/docs/user/metrics.rst b/docs/user/metrics.rst index 64aee694..2846e2e8 100644 --- a/docs/user/metrics.rst +++ b/docs/user/metrics.rst @@ -35,13 +35,13 @@ Ping **fields**: ``reachable``, ``loss``, ``rtt_min``, ``rtt_max``, ``rtt_avg`` **configuration**: ``ping`` -**charts**: ``uptime``, ``packet_loss``, ``rtt`` +**charts**: ``uptime`` (Ping Success Rate), ``packet_loss``, ``rtt`` ================== ================================================== -**Uptime**: +**Ping Success Rate**: -.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/uptime.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/uptime.png +.. figure:: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/ping-success-rate.png + :target: https://github.com/openwisp/openwisp-monitoring/raw/docs/docs/1.1/ping-success-rate.png :align: center **Packet loss**: From 2706f7d6232cbdd2eb5cd15c835cf7e4429681c2 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 15 Jul 2024 19:22:22 +0530 Subject: [PATCH 29/42] [skip ci] Updated docs website URL --- README.rst | 16 ++++++++-------- docs/developer/installation.rst | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 3108d1e2..4138271f 100644 --- a/README.rst +++ b/README.rst @@ -50,23 +50,23 @@ automation solutions can be built on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: - `openwisp-controller - `_: network and WiFi + `_: network and WiFi controller: provisioning, configuration management, x509 PKI management and more; works on OpenWrt, but designed to work also on other systems. - `openwisp-network-topology - `_: provides way + `_: provides way to collect and visualize network topology data from dynamic mesh routing daemons or other network software (eg: OpenVPN); it can be used in conjunction with openwisp-monitoring to get a better idea of the state of the network - `openwisp-firmware-upgrader - `_: automated + `_: automated firmware upgrades (single device or mass network upgrades) -- `openwisp-radius `_: based +- `openwisp-radius `_: based on FreeRADIUS, allows to implement network access authentication systems like 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 (802.11u) -- `openwisp-ipam `_: it allows +- `openwisp-ipam `_: it allows to manage the IP address space of networks **For a more complete overview of the OpenWISP modules and architecture**, @@ -77,15 +77,15 @@ see the `OpenWISP Architecture Overview :align: center For a complete overview of features, refer to the `Monitoring: Features -`_ +`_ section of the OpenWISP documentation. Documentation ------------- - `Developer documentation - `_ -- `User documentation `_ + `_ +- `User documentation `_ Contributing ------------ diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 8390c8d0..5eaffe3a 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -143,7 +143,7 @@ Install and Run on Docker This Docker image is for development purposes only. For the official OpenWISP Docker images, see: `docker-openwisp - `_. + `_. Build from the Dockerfile: From 9e73831fd8bc9f69b2bf169342f0851883232302 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 15 Jul 2024 20:44:40 -0400 Subject: [PATCH 30/42] [docs] Table of contents, consistency, fixes [skip ci] --- docs/developer/extending.rst | 15 ++++++++++----- docs/developer/index.rst | 4 ++-- docs/developer/installation.rst | 9 ++++++--- docs/developer/utils.rst | 4 ++-- docs/index.rst | 2 +- docs/user/checks.rst | 4 ++++ docs/user/configuring-iperf3-check.rst | 4 ++++ ...s.rst => device-checks-and-alert-settings.rst} | 4 ++-- docs/user/metrics.rst | 4 ++++ docs/user/quickstart.rst | 4 ++++ docs/user/rest-api.rst | 4 ++++ 11 files changed, 43 insertions(+), 15 deletions(-) rename docs/user/{adding-checks-and-alertsettings.rst => device-checks-and-alert-settings.rst} (97%) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 00094b4f..f1324f6f 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -3,8 +3,7 @@ Extending OpenWISP Monitoring .. include:: ../partials/developer-docs.rst -One of the core values of the OpenWISP project is `Software Reusability -`_, +One of the core values of the OpenWISP project is :ref:`Software Reusability `, for this reason *openwisp-monitoring* provides a set of base classes which can be imported, extended and reused to create derivative apps. @@ -22,9 +21,15 @@ and the ``sample apps`` namely `sample_check will guide you in the correct direction: just replicate and adapt that code to get a basic derivative of *openwisp-monitoring* working. -**Premise**: if you plan on using a customized version of this module, we -suggest to start with it since the beginning, because migrating your data -from the default module to your extended version may be time consuming. +.. important:: + + If you plan on using a customized version of this module, we suggest + to start with it since the beginning, because migrating your data from + the default module to your extended version may be time consuming. + +.. contents:: **Table of Contents**: + :depth: 2 + :local: 1. Initialize your Custom Module -------------------------------- diff --git a/docs/developer/index.rst b/docs/developer/index.rst index f632316f..99915fc9 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -1,5 +1,5 @@ -Developer Docs Index -==================== +Developer Docs +============== .. include:: ../partials/developer-docs.rst diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 5eaffe3a..2e9d6414 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -3,6 +3,10 @@ Developer Installation Instructions .. include:: ../partials/developer-docs.rst +.. contents:: **Table of contents**: + :depth: 2 + :local: + Dependencies ------------ @@ -94,7 +98,7 @@ Launch development server: ./manage.py runserver 0.0.0.0:8000 -You can access the admin interface at http://127.0.0.1:8000/admin/. +You can access the admin interface at ``http://127.0.0.1:8000/admin/``. Run tests with: @@ -142,8 +146,7 @@ Install and Run on Docker This Docker image is for development purposes only. - For the official OpenWISP Docker images, see: `docker-openwisp - `_. + For the official OpenWISP Docker images, see: :doc:`/docker/index`. Build from the Dockerfile: diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index d24aa64e..9b9efff6 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -3,8 +3,8 @@ Code Utilities .. include:: ../partials/developer-docs.rst -.. contents:: - :depth: 2 +.. contents:: **Table of contents**: + :depth: 1 :local: Registering / Unregistering Metric Configuration diff --git a/docs/index.rst b/docs/index.rst index 712f3484..794577a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,7 @@ For a comprehensive overview of features, please refer to the ./user/device-health-status.rst ./user/metrics.rst ./user/checks.rst - ./user/adding-checks-and-alertsettings.rst + ./user/device-checks-and-alert-settings ./user/configuring-iperf3-check.rst ./user/dashboard-monitoring-charts.rst ./user/wifi-sessions.rst diff --git a/docs/user/checks.rst b/docs/user/checks.rst index 9bc41cb7..c57ba8f9 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -1,6 +1,10 @@ Checks ====== +.. contents:: **Table of contents**: + :depth: 2 + :local: + .. _ping_check: Ping diff --git a/docs/user/configuring-iperf3-check.rst b/docs/user/configuring-iperf3-check.rst index 0a2a9f1b..d3c44b93 100644 --- a/docs/user/configuring-iperf3-check.rst +++ b/docs/user/configuring-iperf3-check.rst @@ -1,6 +1,10 @@ Configuring Iperf3 Check ======================== +.. contents:: **Table of contents**: + :depth: 2 + :local: + 1. Make Sure Iperf3 is Installed on the Device ---------------------------------------------- diff --git a/docs/user/adding-checks-and-alertsettings.rst b/docs/user/device-checks-and-alert-settings.rst similarity index 97% rename from docs/user/adding-checks-and-alertsettings.rst rename to docs/user/device-checks-and-alert-settings.rst index a6949517..ae7e73e5 100644 --- a/docs/user/adding-checks-and-alertsettings.rst +++ b/docs/user/device-checks-and-alert-settings.rst @@ -1,5 +1,5 @@ -Adding Checks and Alert Settings from the Device Page -===================================================== +Managing Device Checks & Alert Settings +======================================= We can add checks and define alert settings directly from the **device page**. diff --git a/docs/user/metrics.rst b/docs/user/metrics.rst index 2846e2e8..b94d3898 100644 --- a/docs/user/metrics.rst +++ b/docs/user/metrics.rst @@ -1,6 +1,10 @@ Metrics ======= +.. contents:: **Table of contents**: + :depth: 2 + :local: + .. _device_status: Device Status diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index e93ab467..d613fed1 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,6 +1,10 @@ Quickstart Guide ================ +.. contents:: **Table of contents**: + :depth: 2 + :local: + .. _install_monitoring_packages_on_device: Install Monitoring Packages on the Device diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 4cff9755..47b09c97 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -1,6 +1,10 @@ REST API Reference ================== +.. contents:: **Table of contents**: + :depth: 1 + :local: + .. _monitoring_live_documentation: Live Documentation From cda6886eb192d194790ff9ef58acffbd47d8ae91 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 20:55:07 -0400 Subject: [PATCH 31/42] [docs] Moved quickstart section to main docs --- docs/user/quickstart.rst | 98 ++++------------------------------------ 1 file changed, 8 insertions(+), 90 deletions(-) diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index d613fed1..becba743 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -1,5 +1,5 @@ -Quickstart Guide -================ +Quick Start Guide +================= .. contents:: **Table of contents**: :depth: 2 @@ -10,99 +10,17 @@ Quickstart Guide Install Monitoring Packages on the Device ----------------------------------------- -:doc:`Install the openwrt-openwisp-monitoring packages +First of all, :doc:`Install the OpenWrt Monitoring Agent ` on your device. -These packages collect and send the monitoring data from the device to -OpenWISP Monitoring and are required to collect :doc:`metrics <./metrics>` -like interface traffic, WiFi clients, CPU load, memory usage, etc. +The agent is responsible for collecting some of the :doc:`monitoring +metrics <./metrics>` from the device and sending these to the server. It's +required to collect interface traffic, WiFi clients, CPU load, memory +usage, storage usage, cellular signal strength, etc. .. _openwisp_reach_devices: Make Sure OpenWISP can Reach your Devices ----------------------------------------- -In order to perform :doc:`active checks <./checks>` and other actions like -:doc:`triggering the push of configuration changes -`, :doc:`executing shell commands -`, or :doc:`performing firmware upgrades -`, **the OpenWISP server needs to be -able to reach the network devices**. - -There are mainly two deployment scenarios for OpenWISP: - -1. the OpenWISP server is deployed on the public internet and the devices - are geographically distributed across different locations: **in this - case a management tunnel is needed** -2. the OpenWISP server is deployed on a computer/server which is located - in the same Layer 2 network (that is, in the same LAN) where the - devices are located. **In this case a management tunnel is NOT needed** - -1. Public Internet Deployment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is the most common scenario: - -- the OpenWISP server is deployed to the public internet, hence the server - has a public IPv4 (and IPv6) address and usually a valid SSL certificate - provided by Let's Encrypt or another SSL provider -- the network devices are geographically distributed across different - locations (different cities, different regions, different countries) - -In this scenario, the OpenWISP application will not be able to reach the -devices unless a management tunnel is used, for that reason having a -management VPN like OpenVPN, Wireguard, ZeroTier or any other tunneling -solution is paramount, not only to allow OpenWISP to work properly, but -also to be able to perform debugging and troubleshooting when needed. - -In this scenario, the following requirements are needed: - -- a VPN server must be installed in a way that the OpenWISP server can - reach the VPN peers. For more information on how to do this via OpenWISP - please refer to the following sections: - - - :doc:`OpenVPN tunnel automation ` - - :doc:`Wireguard tunnel automation ` - - If you prefer to use other tunneling solutions (L2TP, Softether, etc.) - and know how to configure those solutions on your own, that's totally - fine as well. - - If the OpenWISP server is connected to a network infrastructure which - allows it to reach the devices via pre-existing tunneling or Intranet - solutions (eg: MPLS, SD-WAN), then setting up a VPN server is not - needed, as long as there's a dedicated interface on OpenWrt which has an - IP address assigned to it and which is reachable from the OpenWISP - server. - -- The devices must be configured to join the management tunnel - automatically, either via a pre-existing configuration in the firmware - or via an :doc:`OpenWISP Template `. -- The :doc:`openwisp-config ` agent on the - devices must be configured to specify the ``management_interface`` - option, the agent will communicate the IP of the management interface to - the OpenWISP Server and OpenWISP will use the management IP for reaching - the device. - - For example, if the *management interface* is named ``tun0``, the - openwisp-config configuration should look like the following example: - -.. code-block:: text - - # In /etc/config/openwisp on the device - - config controller 'http' - # ... other configuration directives ... - option management_interface 'tun0' - -2. LAN Deployment -~~~~~~~~~~~~~~~~~ - -When the OpenWISP server and the network devices are deployed in the same -L2 network (eg: an office LAN) and the OpenWISP server is reachable on the -LAN address, OpenWISP can then use the **Last IP** field of the devices to -reach them. - -In this scenario it's necessary to set the -:ref:`"OPENWISP_MONITORING_MANAGEMENT_IP_ONLY" -` setting to ``False``. +Please make sure that :doc:`OpenWISP can reach your devices `. From 933a4e051f413d564365a4a81e0712103e82526e Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 20:55:22 -0400 Subject: [PATCH 32/42] [docs] Point to monitoring agent directly --- docs/user/metrics.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/metrics.rst b/docs/user/metrics.rst index b94d3898..79d44704 100644 --- a/docs/user/metrics.rst +++ b/docs/user/metrics.rst @@ -295,9 +295,9 @@ Monitoring can be divided in two categories: continuously sends network requests to the devices and store the results; 2. **metrics collected passively by OpenWISP**: these metrics are sent by - the :ref:`openwrt-openwisp-monitoring agent - ` installed on the network - devices and are collected by OpenWISP via its REST API. + the :doc:`OpenWrt Monitoring Agent ` + installed on the network devices and are collected by OpenWISP via its + REST API. The :doc:`checks` section of the documentation lists the currently implemented **active checks**. From 1c74f97147140c7318d9244eeb86cadb60c3c1fa Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 20:55:28 -0400 Subject: [PATCH 33/42] [docs] Reformatted [skip ci] --- docs/developer/extending.rst | 7 +- docs/developer/utils.rst | 156 ++++++++++++++++++----------------- docs/user/checks.rst | 8 +- docs/user/intro.rst | 16 ++-- docs/user/metrics.rst | 3 +- docs/user/rest-api.rst | 4 +- docs/user/wifi-sessions.rst | 11 ++- 7 files changed, 104 insertions(+), 101 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index f1324f6f..5f9edb14 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -3,9 +3,10 @@ Extending OpenWISP Monitoring .. include:: ../partials/developer-docs.rst -One of the core values of the OpenWISP project is :ref:`Software Reusability `, -for this reason *openwisp-monitoring* provides a set of base classes which -can be imported, extended and reused to create derivative apps. +One of the core values of the OpenWISP project is :ref:`Software +Reusability `, for this reason +*openwisp-monitoring* provides a set of base classes which can be +imported, extended and reused to create derivative apps. In order to implement your custom version of *openwisp-monitoring*, you need to perform the steps described in the rest of this section. diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 9b9efff6..31d85bb5 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -39,98 +39,100 @@ An example usage has been shown below. # Define configuration of your metric metric_config = { - 'label': _('Ping'), - 'name': 'Ping', - 'key': 'ping', - 'field_name': 'reachable', - 'related_fields': ['loss', 'rtt_min', 'rtt_max', 'rtt_avg'], - 'charts': { - 'uptime': { - 'type': 'bar', - 'title': _('Ping Success Rate'), - 'description': _( - 'A value of 100% means reachable, 0% means unreachable, values in ' - 'between 0% and 100% indicate the average reachability in the ' - 'period observed. Obtained with the fping linux program.' + "label": _("Ping"), + "name": "Ping", + "key": "ping", + "field_name": "reachable", + "related_fields": ["loss", "rtt_min", "rtt_max", "rtt_avg"], + "charts": { + "uptime": { + "type": "bar", + "title": _("Ping Success Rate"), + "description": _( + "A value of 100% means reachable, 0% means unreachable, values in " + "between 0% and 100% indicate the average reachability in the " + "period observed. Obtained with the fping linux program." ), - 'summary_labels': [_('Average Ping Success Rate')], - 'unit': '%', - 'order': 200, - 'colorscale': { - 'max': 100, - 'min': 0, - 'label': _('Rate'), - 'scale': [ - [[0, '#c13000'], - [0.1,'cb7222'], - [0.5,'#deed0e'], - [0.9, '#7db201'], - [1, '#498b26']], + "summary_labels": [_("Average Ping Success Rate")], + "unit": "%", + "order": 200, + "colorscale": { + "max": 100, + "min": 0, + "label": _("Rate"), + "scale": [ + [ + [0, "#c13000"], + [0.1, "cb7222"], + [0.5, "#deed0e"], + [0.9, "#7db201"], + [1, "#498b26"], + ], ], - 'map': [ - [100, '#498b26', _('Flawless')], - [90, '#7db201', _('Mostly Reachable')], - [50, '#deed0e', _('Partly Reachable')], - [10, '#cb7222', _('Mostly Unreachable')], - [None, '#c13000', _('Unreachable')], + "map": [ + [100, "#498b26", _("Flawless")], + [90, "#7db201", _("Mostly Reachable")], + [50, "#deed0e", _("Partly Reachable")], + [10, "#cb7222", _("Mostly Unreachable")], + [None, "#c13000", _("Unreachable")], ], - 'fixed_value': 100, + "fixed_value": 100, }, - 'query': chart_query['uptime'], + "query": chart_query["uptime"], }, - 'packet_loss': { - 'type': 'bar', - 'title': _('Packet loss'), - 'description': _( - 'Indicates the percentage of lost packets observed in ICMP probes. ' - 'Obtained with the fping linux program.' + "packet_loss": { + "type": "bar", + "title": _("Packet loss"), + "description": _( + "Indicates the percentage of lost packets observed in ICMP probes. " + "Obtained with the fping linux program." ), - 'summary_labels': [_('Average packet loss')], - 'unit': '%', - 'colors': '#d62728', - 'order': 210, - 'query': chart_query['packet_loss'], + "summary_labels": [_("Average packet loss")], + "unit": "%", + "colors": "#d62728", + "order": 210, + "query": chart_query["packet_loss"], }, - 'rtt': { - 'type': 'scatter', - 'title': _('Round Trip Time'), - 'description': _( - 'Round trip time observed in ICMP probes, measuered in milliseconds.' + "rtt": { + "type": "scatter", + "title": _("Round Trip Time"), + "description": _( + "Round trip time observed in ICMP probes, measuered in milliseconds." ), - 'summary_labels': [ - _('Average RTT'), - _('Average Max RTT'), - _('Average Min RTT'), + "summary_labels": [ + _("Average RTT"), + _("Average Max RTT"), + _("Average Min RTT"), ], - 'unit': _(' ms'), - 'order': 220, - 'query': chart_query['rtt'], + "unit": _(" ms"), + "order": 220, + "query": chart_query["rtt"], }, }, - 'alert_settings': {'operator': '<', 'threshold': 1, 'tolerance': 0}, - 'notification': { - 'problem': { - 'verbose_name': 'Ping PROBLEM', - 'verb': 'cannot be reached anymore', - 'level': 'warning', - 'email_subject': _( - '[{site.name}] {notification.target} is not reachable' + "alert_settings": {"operator": "<", "threshold": 1, "tolerance": 0}, + "notification": { + "problem": { + "verbose_name": "Ping PROBLEM", + "verb": "cannot be reached anymore", + "level": "warning", + "email_subject": _( + "[{site.name}] {notification.target} is not reachable" ), - 'message': _( - 'The device [{notification.target}] {notification.verb} anymore by our ping ' - 'messages.' + "message": _( + "The device [{notification.target}] {notification.verb} anymore by our ping " + "messages." ), }, - 'recovery': { - 'verbose_name': 'Ping RECOVERY', - 'verb': 'has become reachable', - 'level': 'info', - 'email_subject': _( - '[{site.name}] {notification.target} is reachable again' + "recovery": { + "verbose_name": "Ping RECOVERY", + "verb": "has become reachable", + "level": "info", + "email_subject": _( + "[{site.name}] {notification.target} is reachable again" ), - 'message': _( - 'The device [{notification.target}] {notification.verb} again by our ping ' - 'messages.' + "message": _( + "The device [{notification.target}] {notification.verb} again by our ping " + "messages." ), }, }, diff --git a/docs/user/checks.rst b/docs/user/checks.rst index c57ba8f9..6b7b0910 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -10,10 +10,10 @@ Checks Ping ---- -This check returns information on Ping Success Rate and RTT (Round trip time). -It creates charts like Ping Success Rate, Packet Loss and RTT. -These metrics are collected using the ``fping`` Linux program. -You may choose to disable auto creation of this check by setting +This check returns information on Ping Success Rate and RTT (Round trip +time). It creates charts like Ping Success Rate, Packet Loss and RTT. +These metrics are collected using the ``fping`` Linux program. You may +choose to disable auto creation of this check by setting :ref:`openwisp_monitoring_auto_ping` to ``False``. You can change the default values used for ping checks using diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 439ac9b5..05aec09b 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -16,14 +16,14 @@ OpenWISP provides the following monitoring capabilities: like uptime, RAM status, CPU load averages, Interface properties and addresses, WiFi interface status and associated clients, Neighbors information, DHCP Leases, Disk/Flash status -- Monitoring charts for :ref:`ping success rate `, :ref:`packet loss - `, :ref:`round trip time (latency) `, - :ref:`associated wifi clients `, :ref:`interface traffic - `, :ref:`RAM usage `, :ref:`CPU load `, - :ref:`flash/disk usage `, mobile signal (LTE/UMTS/GSM - :ref:`signal strength `, :ref:`signal quality - `, :ref:`access technology in use - `), :ref:`bandwidth `, +- Monitoring charts for :ref:`ping success rate `, + :ref:`packet loss `, :ref:`round trip time (latency) + `, :ref:`associated wifi clients `, + :ref:`interface traffic `, :ref:`RAM usage `, + :ref:`CPU load `, :ref:`flash/disk usage `, mobile + signal (LTE/UMTS/GSM :ref:`signal strength `, + :ref:`signal quality `, :ref:`access technology + in use `), :ref:`bandwidth `, :ref:`transferred data `, :ref:`restransmits `, :ref:`jitter `, :ref:`datagram `, :ref:`datagram loss ` diff --git a/docs/user/metrics.rst b/docs/user/metrics.rst index 79d44704..aaff7bc6 100644 --- a/docs/user/metrics.rst +++ b/docs/user/metrics.rst @@ -39,7 +39,8 @@ Ping **fields**: ``reachable``, ``loss``, ``rtt_min``, ``rtt_max``, ``rtt_avg`` **configuration**: ``ping`` -**charts**: ``uptime`` (Ping Success Rate), ``packet_loss``, ``rtt`` +**charts**: ``uptime`` (Ping Success Rate), ``packet_loss``, + ``rtt`` ================== ================================================== **Ping Success Rate**: diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 47b09c97..70492716 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -192,8 +192,8 @@ local time will be used. The ``time`` parameter was added to support :ref:`resilient collection and sending of data by the OpenWISP Monitoring Agent -`, -this feature allows sending data collected while the device is offline. +`, this feature allows sending +data collected while the device is offline. List Nearby Devices ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/wifi-sessions.rst b/docs/user/wifi-sessions.rst index 5fa95fe7..83b8c657 100644 --- a/docs/user/wifi-sessions.rst +++ b/docs/user/wifi-sessions.rst @@ -37,12 +37,11 @@ sessions older than a pre-configured number of days. .. note:: If you have deployed OpenWISP using :doc:`ansible-openwisp2 - ` or :doc:`docker-openwisp - `, then this feature has - been already configured for you. Refer to the documentation of your - deployment method to know the default value. This section is only for - reference for users who wish to customize OpenWISP, or who have - deployed OpenWISP in a different way. + ` or :doc:`docker-openwisp `, then this + feature has been already configured for you. Refer to the + documentation of your deployment method to know the default value. + This section is only for reference for users who wish to customize + OpenWISP, or who have deployed OpenWISP in a different way. The celery task takes only one argument, i.e. number of days. You can provide any number of days in `args` key while configuring From bac1e382c21eab3c463b9dc4ead99facb5d91205 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 22 Jul 2024 19:23:03 -0400 Subject: [PATCH 34/42] [docs] Added architecture diagram [skip ci] --- .../architecture-v2-openwisp-monitoring.png | Bin 0 -> 392124 bytes docs/index.rst | 37 ++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 docs/images/architecture-v2-openwisp-monitoring.png diff --git a/docs/images/architecture-v2-openwisp-monitoring.png b/docs/images/architecture-v2-openwisp-monitoring.png new file mode 100644 index 0000000000000000000000000000000000000000..5a67d8033791421751c5476ad11df091e9a20a78 GIT binary patch literal 392124 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�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

=jsbilDS9Qnp3HJm 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ⅇ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 st} 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

lgUw4Z&dn~fbctuj|%-ZcUXt~ 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. +.. seealso:: -The OpenWISP Monitoring module leverages the capabilities of Python and -the Django Framework to provide OpenWISP with robust network monitoring -features. Designed to be extensible, programmable, scalable, and -user-friendly, this module automates monitoring checks, alerts, and metric -collection, ensuring efficient and comprehensive network management. + **Source code**: `github.com/openwisp/openwisp-monitoring + `_. -For a comprehensive overview of features, please refer to the -:doc:`user/intro` page. +The OpenWISP Monitoring module leverages the capabilities of Python and the Django +Framework to provide OpenWISP with robust network monitoring features. Designed to be +extensible, programmable, scalable, and user-friendly, this module automates monitoring +checks, alerts, and metric collection, ensuring efficient and comprehensive network +management. + +For a comprehensive overview of its features, please refer to the :doc:`user/intro` +page. + +.. figure:: images/architecture-v2-openwisp-monitoring.png + :target: ../_images/architecture-v2-openwisp-monitoring.png + :align: center + :alt: OpenWISP Architecture: Monitoring module + + **OpenWISP Architecture: highlighted monitoring module** + +.. important:: + + For an enhanced viewing experience, open the image above in a new browser tab. + + Refer to :doc:`/general/architecture` for more information. .. toctree:: - :caption: User Docs + :caption: Monitoring Module Usage Docs :maxdepth: 1 ./user/intro.rst @@ -31,7 +46,7 @@ For a comprehensive overview of features, please refer to the ./user/management-commands.rst .. toctree:: - :caption: Developer Docs + :caption: Monitoring Module Developer Docs :maxdepth: 2 Developer Docs Index From de8f54afa68d232bcee94789dec7532e3acaf91f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Tue, 23 Jul 2024 19:01:50 -0400 Subject: [PATCH 35/42] [docs] Minor improvement to index --- docs/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index b150e9ea..fb3d5125 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,6 +15,9 @@ management. For a comprehensive overview of its features, please refer to the :doc:`user/intro` page. +The following diagram illustrates the role of the Monitoring module within the OpenWISP +architecture. + .. figure:: images/architecture-v2-openwisp-monitoring.png :target: ../_images/architecture-v2-openwisp-monitoring.png :align: center From 45f67aad474dbe6d606fba8927525bf5bbb8aea1 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Tue, 23 Jul 2024 19:02:27 -0400 Subject: [PATCH 36/42] [docs] Reformatted index --- docs/index.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index fb3d5125..357c6181 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,17 +6,17 @@ Monitoring **Source code**: `github.com/openwisp/openwisp-monitoring `_. -The OpenWISP Monitoring module leverages the capabilities of Python and the Django -Framework to provide OpenWISP with robust network monitoring features. Designed to be -extensible, programmable, scalable, and user-friendly, this module automates monitoring -checks, alerts, and metric collection, ensuring efficient and comprehensive network -management. +The OpenWISP Monitoring module leverages the capabilities of Python and +the Django Framework to provide OpenWISP with robust network monitoring +features. Designed to be extensible, programmable, scalable, and +user-friendly, this module automates monitoring checks, alerts, and metric +collection, ensuring efficient and comprehensive network management. -For a comprehensive overview of its features, please refer to the :doc:`user/intro` -page. +For a comprehensive overview of its features, please refer to the +:doc:`user/intro` page. -The following diagram illustrates the role of the Monitoring module within the OpenWISP -architecture. +The following diagram illustrates the role of the Monitoring module within +the OpenWISP architecture. .. figure:: images/architecture-v2-openwisp-monitoring.png :target: ../_images/architecture-v2-openwisp-monitoring.png @@ -27,7 +27,8 @@ architecture. .. important:: - For an enhanced viewing experience, open the image above in a new browser tab. + For an enhanced viewing experience, open the image above in a new + browser tab. Refer to :doc:`/general/architecture` for more information. From 72ef0a5177881655ff7aba1b92988c36c1add60d Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 27 Jul 2024 14:56:07 -0400 Subject: [PATCH 37/42] [docs] Reformatted [skip ci] --- README.rst | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 4138271f..c02d3266 100644 --- a/README.rst +++ b/README.rst @@ -49,25 +49,25 @@ automation solutions can be built on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: -- `openwisp-controller - `_: network and WiFi - controller: provisioning, configuration management, x509 PKI management - and more; works on OpenWrt, but designed to work also on other systems. +- `openwisp-controller `_: + network and WiFi controller: provisioning, configuration management, + x509 PKI management and more; works on OpenWrt, but designed to work + also on other systems. - `openwisp-network-topology - `_: provides way - to collect and visualize network topology data from dynamic mesh routing + `_: provides way to + collect and visualize network topology data from dynamic mesh routing daemons or other network software (eg: OpenVPN); it can be used in conjunction with openwisp-monitoring to get a better idea of the state of the network - `openwisp-firmware-upgrader - `_: automated - firmware upgrades (single device or mass network upgrades) -- `openwisp-radius `_: based - on FreeRADIUS, allows to implement network access authentication systems - like 802.1x WPA2 Enterprise, captive portal authentication, Hotspot 2.0 - (802.11u) -- `openwisp-ipam `_: it allows - to manage the IP address space of networks + `_: automated firmware + upgrades (single device or mass network upgrades) +- `openwisp-radius `_: + based on FreeRADIUS, allows to implement network access authentication + systems like 802.1x WPA2 Enterprise, captive portal authentication, + Hotspot 2.0 (802.11u) +- `openwisp-ipam `_: it allows to + manage the IP address space of networks **For a more complete overview of the OpenWISP modules and architecture**, see the `OpenWISP Architecture Overview @@ -77,8 +77,8 @@ see the `OpenWISP Architecture Overview :align: center For a complete overview of features, refer to the `Monitoring: Features -`_ -section of the OpenWISP documentation. +`_ section of the +OpenWISP documentation. Documentation ------------- From c72a7faffcd1f90bd8450f8d02ab58b12fcccb53 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 27 Jul 2024 15:42:16 -0400 Subject: [PATCH 38/42] [docs] Fixed doc reference [skip ci] --- docs/user/checks.rst | 4 ++-- docs/user/intro.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/checks.rst b/docs/user/checks.rst index 6b7b0910..894756d0 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -48,7 +48,7 @@ This check is **disabled by default**. You can enable auto creation of this check by setting the :ref:`openwisp_monitoring_auto_iperf3` to ``True``. -You can also :doc:`add the iperf3 check ` +You can also :doc:`add the iperf3 check ` directly from the device page. It also supports tuning of various parameters. You can change the @@ -60,5 +60,5 @@ rsa_publc_key etc) using the When setting :ref:`openwisp_monitoring_auto_iperf3` to ``True``, you may need to update the :doc:`metric configuration - ` to enable alerts for the iperf3 + ` to enable alerts for the iperf3 check. diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 05aec09b..824dc4d2 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -37,7 +37,7 @@ OpenWISP provides the following monitoring capabilities: - Possibility to configure additional :ref:`Metrics ` and :ref:`Charts ` -- :doc:`Extensible active check system `: +- :doc:`Extensible active check system `: it's possible to write additional checks that are run periodically using python classes - Extensible :ref:`metrics ` and :ref:`charts From c2d740309998bcb020874c47ac5d9880017c5cd8 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 29 Jul 2024 22:39:45 -0400 Subject: [PATCH 39/42] [docs:qa] Reformatted --- docs/user/checks.rst | 4 ++-- docs/user/intro.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/user/checks.rst b/docs/user/checks.rst index 894756d0..83328d49 100644 --- a/docs/user/checks.rst +++ b/docs/user/checks.rst @@ -48,8 +48,8 @@ This check is **disabled by default**. You can enable auto creation of this check by setting the :ref:`openwisp_monitoring_auto_iperf3` to ``True``. -You can also :doc:`add the iperf3 check ` -directly from the device page. +You can also :doc:`add the iperf3 check +` directly from the device page. It also supports tuning of various parameters. You can change the parameters used for iperf3 checks (e.g. timing, port, username, password, diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 824dc4d2..bd4f0770 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -37,9 +37,9 @@ OpenWISP provides the following monitoring capabilities: - Possibility to configure additional :ref:`Metrics ` and :ref:`Charts ` -- :doc:`Extensible active check system `: - it's possible to write additional checks that are run periodically using - python classes +- :doc:`Extensible active check system + `: it's possible to write additional + checks that are run periodically using python classes - Extensible :ref:`metrics ` and :ref:`charts `: it's possible to define new metrics and new charts From fdaef6c861acf6db6ff1f7315e8d89711cf2fe38 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 29 Jul 2024 22:40:08 -0400 Subject: [PATCH 40/42] [qa] Reformatted docstrings --- openwisp_monitoring/check/base/models.py | 30 +++---- openwisp_monitoring/check/classes/base.py | 4 +- openwisp_monitoring/check/classes/iperf3.py | 44 +++------- openwisp_monitoring/check/classes/ping.py | 24 ++---- openwisp_monitoring/check/tasks.py | 36 ++++---- .../check/tests/test_iperf3.py | 2 +- .../check/tests/test_models.py | 4 +- openwisp_monitoring/check/utils.py | 5 +- openwisp_monitoring/db/backends/__init__.py | 8 +- .../db/backends/influxdb/client.py | 24 +++--- openwisp_monitoring/device/admin.py | 4 +- openwisp_monitoring/device/api/views.py | 32 ++++--- openwisp_monitoring/device/apps.py | 8 +- openwisp_monitoring/device/base/models.py | 64 ++++++-------- .../0001_squashed_0002_devicemonitoring.py | 4 +- openwisp_monitoring/device/tasks.py | 12 +-- openwisp_monitoring/device/tests/__init__.py | 8 +- .../device/tests/test_admin.py | 12 +-- openwisp_monitoring/device/tests/test_api.py | 4 +- .../device/tests/test_models.py | 8 +- .../device/tests/test_recovery.py | 13 +-- .../device/tests/test_settings.py | 4 +- openwisp_monitoring/device/utils.py | 8 +- openwisp_monitoring/device/writer.py | 35 +++----- openwisp_monitoring/monitoring/api/views.py | 9 +- openwisp_monitoring/monitoring/base/models.py | 83 +++++++++---------- .../monitoring/configuration.py | 8 +- .../influxdb/influxdb_alter_structure_0006.py | 14 ++-- openwisp_monitoring/monitoring/tasks.py | 29 +++---- .../monitoring/tests/test_charts.py | 4 +- .../tests/test_monitoring_notifications.py | 12 +-- openwisp_monitoring/views.py | 13 +-- setup.py | 4 +- tests/openwisp2/sample_check/models.py | 7 +- 34 files changed, 237 insertions(+), 343 deletions(-) diff --git a/openwisp_monitoring/check/base/models.py b/openwisp_monitoring/check/base/models.py index 2f2e05b2..c5156698 100644 --- a/openwisp_monitoring/check/base/models.py +++ b/openwisp_monitoring/check/base/models.py @@ -81,23 +81,17 @@ def full_clean(self, *args, **kwargs): @cached_property def check_class(self): - """ - returns check class - """ + """Returns the check class.""" return import_string(self.check_type) @cached_property def check_instance(self): - """ - returns check class instance - """ + """Returns the check class instance.""" check_class = self.check_class return check_class(check=self, params=self.params) def perform_check(self, store=True): - """ - initiates check instance and calls its check method - """ + """Initializes check instance and calls the check method.""" if ( hasattr(self.content_object, 'organization_id') and self.content_object.organization.is_active is False @@ -112,9 +106,9 @@ def perform_check_delayed(self, duration=0): def auto_ping_receiver(sender, instance, created, **kwargs): - """ - Implements OPENWISP_MONITORING_AUTO_PING - The creation step is executed in the background + """Implements OPENWISP_MONITORING_AUTO_PING. + + 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 @@ -130,9 +124,9 @@ def auto_ping_receiver(sender, instance, created, **kwargs): def auto_config_check_receiver(sender, instance, created, **kwargs): - """ - Implements OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_CHECK - The creation step is executed in the background + """Implements OPENWISP_MONITORING_AUTO_DEVICE_CONFIG_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 @@ -148,9 +142,9 @@ def auto_config_check_receiver(sender, instance, created, **kwargs): def auto_iperf3_check_receiver(sender, instance, created, **kwargs): - """ - Implements OPENWISP_MONITORING_AUTO_IPERF3 - The creation step is executed in the background + """Implements OPENWISP_MONITORING_AUTO_IPERF3. + + 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 diff --git a/openwisp_monitoring/check/classes/base.py b/openwisp_monitoring/check/classes/base.py index 1c16f520..12cf51c9 100644 --- a/openwisp_monitoring/check/classes/base.py +++ b/openwisp_monitoring/check/classes/base.py @@ -31,9 +31,7 @@ def check(self, store=True): raise NotImplementedError def _get_or_create_metric(self, configuration=None): - """ - Gets or creates metric - """ + """Gets or creates metric.""" check = self.check_instance if check.object_id and check.content_type_id: obj_id = check.object_id diff --git a/openwisp_monitoring/check/classes/iperf3.py b/openwisp_monitoring/check/classes/iperf3.py index 4af2c5b7..fe9548aa 100644 --- a/openwisp_monitoring/check/classes/iperf3.py +++ b/openwisp_monitoring/check/classes/iperf3.py @@ -285,9 +285,7 @@ def _run_iperf3_check(self, store, server, time): return result def _get_check_commands(self, server): - """ - Returns tcp & udp commands for iperf3 check - """ + """Returns tcp & udp commands for iperf3 check.""" username = self._get_param('username', 'username.default') port = self._get_param( 'client_options.port', 'client_options.properties.port.default' @@ -357,9 +355,7 @@ def _get_check_commands(self, server): return command_tcp, command_udp def _get_iperf3_test_conditions(self): - """ - Returns iperf3 check test conditions (rev_or_bidir, end_condition) - """ + """Returns iperf3 check test conditions (rev_or_bidir, end_condition).""" time = self._get_param( 'client_options.time', 'client_options.properties.time.default' ) @@ -397,19 +393,17 @@ def _get_iperf3_test_conditions(self): return rev_or_bidir, test_end_condition def _get_compelete_rsa_key(self, key): - """ - Returns RSA key with proper format - """ + """Returns RSA key with proper format.""" pem_prefix = '-----BEGIN PUBLIC KEY-----\n' pem_suffix = '\n-----END PUBLIC KEY-----' key = key.strip() return f'{pem_prefix}{key}{pem_suffix}' def _deep_get(self, dictionary, keys, default=None): - """ - Returns dict key value using dict & - it's dot_key string ie. key1.key2_nested.key3_nested - if found otherwise returns default + """Returns dict key value using dict and it's dot_key string, + + ie: key1.key2_nested.key3_nested, if found, otherwise returns + default. """ return reduce( lambda d, key: d.get(key, default) if isinstance(d, dict) else default, @@ -418,9 +412,7 @@ def _deep_get(self, dictionary, keys, default=None): ) def _get_param(self, conf_key, default_conf_key): - """ - Returns specified param or its default value according to the schema - """ + """Returns specified param or its default value according to the schema.""" org_id = str(self.related_object.organization.id) iperf3_config = app_settings.IPERF3_CHECK_CONFIG @@ -438,9 +430,7 @@ def _get_param(self, conf_key, default_conf_key): return self._deep_get(DEFAULT_IPERF3_CHECK_CONFIG, default_conf_key) def _get_iperf3_result(self, result, exit_code, mode): - """ - Returns iperf3 test result - """ + """Returns iperf3 test result.""" try: result = loads(result) except JSONDecodeError: @@ -501,18 +491,14 @@ def _get_iperf3_result(self, result, exit_code, mode): } def store_result(self, result): - """ - Store result in the DB - """ + """Store result in the DB.""" metric = self._get_metric() copied = result.copy() iperf3_result = copied.pop('iperf3_result') metric.write(iperf3_result, extra_values=copied) def _get_metric(self): - """ - Gets or creates metric - """ + """Gets or creates metric.""" metric, created = self._get_or_create_metric() if created: self._create_alert_settings(metric) @@ -520,17 +506,13 @@ def _get_metric(self): return metric def _create_alert_settings(self, metric): - """ - Creates default iperf3 alert settings with is_active=False - """ + """Creates default iperf3 alert settings with is_active=False.""" alert_settings = AlertSettings(metric=metric, is_active=False) alert_settings.full_clean() alert_settings.save() def _create_charts(self, metric): - """ - Creates iperf3 related charts - """ + """Creates iperf3 related charts.""" charts = [ 'bandwidth', 'transfer', diff --git a/openwisp_monitoring/check/classes/ping.py b/openwisp_monitoring/check/classes/ping.py index 22b316ab..933abf58 100644 --- a/openwisp_monitoring/check/classes/ping.py +++ b/openwisp_monitoring/check/classes/ping.py @@ -120,24 +120,18 @@ def check(self, store=True): return result def store_result(self, result): - """ - store result in the DB - """ + """Stores result in the DB.""" metric = self._get_metric() copied = result.copy() reachable = copied.pop('reachable') metric.write(reachable, extra_values=copied) def _get_param(self, param): - """ - Gets specified param or its default value according to the schema - """ + """Gets specified param or its default value according to the schema.""" return self.params.get(param, self.schema['properties'][param]['default']) def _get_ip(self): - """ - Figures out ip to use or fails raising OperationalError - """ + """Figures out ip to use or fails raising OperationalError.""" device = self.related_object ip = device.management_ip if not ip and not app_settings.MANAGEMENT_IP_ONLY: @@ -145,16 +139,12 @@ def _get_ip(self): return ip def _command(self, command): - """ - Executes command (easier to mock) - """ + """Executes command (easier to mock).""" p = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return p.stdout, p.stderr def _get_metric(self): - """ - Gets or creates metric - """ + """Gets or creates metric.""" metric, created = self._get_or_create_metric() if created: self._create_alert_settings(metric) @@ -167,9 +157,7 @@ def _create_alert_settings(self, metric): alert_settings.save() def _create_charts(self, metric): - """ - Creates device charts if necessary - """ + """Creates device charts if necessary.""" charts = ['uptime', 'packet_loss', 'rtt'] for chart in charts: if chart not in monitoring_settings.AUTO_CHARTS: diff --git a/openwisp_monitoring/check/tasks.py b/openwisp_monitoring/check/tasks.py index 3f56878a..ee2c2b4a 100644 --- a/openwisp_monitoring/check/tasks.py +++ b/openwisp_monitoring/check/tasks.py @@ -20,12 +20,13 @@ def get_check_model(): @shared_task(time_limit=2 * 60 * 60) def run_checks(checks=None): - """ - Retrieves the id of all active checks in chunks of 2000 items - and calls the ``perform_check`` task (defined below) for each of them. + """Runs all the checks. + + Retrieves the id of all active checks in chunks of 2000 items and + calls the ``perform_check`` task (defined below) for each of them. - This allows to enqueue all the checks that need to be performed - and execute them in parallel with multiple workers if needed. + This allows to enqueue all the checks that need to be performed and + execute them in parallel with multiple workers if needed. """ # If checks is None, We should execute all the checks if checks is None: @@ -53,9 +54,10 @@ def run_checks(checks=None): @shared_task(time_limit=30 * 60) def perform_check(uuid): - """ - Retrieves check according to the passed UUID - and calls ``check.perform_check()`` + """Performs check with specified uuid. + + Retrieves check according to the passed UUID and calls the + ``perform_check()`` method. """ try: check = get_check_model().objects.get(pk=uuid) @@ -71,9 +73,10 @@ def perform_check(uuid): def auto_create_ping( model, app_label, object_id, check_model=None, content_type_model=None ): - """ - Called by django signal (dispatch_uid: auto_ping) - registered in check app's apps.py file. + """Implements the auto creation of the ping check. + + Called by django signal (dispatch_uid: auto_ping) registered in check + app's apps.py file. """ Check = check_model or get_check_model() ping_path = 'openwisp_monitoring.check.classes.Ping' @@ -96,8 +99,9 @@ def auto_create_ping( def auto_create_config_check( model, app_label, object_id, check_model=None, content_type_model=None ): - """ - Called by openwisp_monitoring.check.models.auto_config_check_receiver + """Implements the auto creation of the config modified check. + + Called by openwisp_monitoring.check.models.auto_config_check_receiver. """ Check = check_model or get_check_model() config_check_path = 'openwisp_monitoring.check.classes.ConfigApplied' @@ -123,8 +127,10 @@ def auto_create_config_check( def auto_create_iperf3_check( model, app_label, object_id, check_model=None, content_type_model=None ): - """ - Called by openwisp_monitoring.check.models.auto_iperf3_check_receiver + """Implements the auto creation of the iperf3 check. + + Called by the + openwisp_monitoring.check.models.auto_iperf3_check_receiver. """ Check = check_model or get_check_model() iperf3_check_path = 'openwisp_monitoring.check.classes.Iperf3' diff --git a/openwisp_monitoring/check/tests/test_iperf3.py b/openwisp_monitoring/check/tests/test_iperf3.py index 80eb7b2f..941fa473 100644 --- a/openwisp_monitoring/check/tests/test_iperf3.py +++ b/openwisp_monitoring/check/tests/test_iperf3.py @@ -622,7 +622,7 @@ def test_iperf3_check_auth_config(self, mock_warn, mock_exec_command): with patch.object( app_settings, 'IPERF3_CHECK_CONFIG', - iperf3_config + iperf3_config, # It is required to mock "Iperf3.schema" here so that it # uses the updated configuration from "IPERF3_CHECK_CONFIG" setting. ), patch.object(Iperf3, 'schema', get_iperf3_schema()): diff --git a/openwisp_monitoring/check/tests/test_models.py b/openwisp_monitoring/check/tests/test_models.py index c0e5aef8..503a5805 100644 --- a/openwisp_monitoring/check/tests/test_models.py +++ b/openwisp_monitoring/check/tests/test_models.py @@ -176,9 +176,7 @@ def test_config_modified_device_problem(self): self.assertEqual(Notification.objects.count(), 1) def test_config_error(self): - """ - Test that ConfigApplied checks are skipped when device config status is errored - """ + """Test that ConfigApplied checks are skipped when device config status is errored.""" self._create_admin() self.assertEqual(Check.objects.count(), 0) with freeze_time( diff --git a/openwisp_monitoring/check/utils.py b/openwisp_monitoring/check/utils.py index 4897c77a..e37e7ab2 100644 --- a/openwisp_monitoring/check/utils.py +++ b/openwisp_monitoring/check/utils.py @@ -2,8 +2,5 @@ def run_checks_async(): - """ - Calls celery task run_checks - is run in a background worker - """ + """Calls celery task run_checks is run in a background worker.""" run_checks.delay() diff --git a/openwisp_monitoring/db/backends/__init__.py b/openwisp_monitoring/db/backends/__init__.py index 715b1113..3b9e0426 100644 --- a/openwisp_monitoring/db/backends/__init__.py +++ b/openwisp_monitoring/db/backends/__init__.py @@ -24,9 +24,11 @@ def load_backend_module(backend_name=TIMESERIES_DB['BACKEND'], module=None): - """ - Returns database backend module given a fully qualified database backend name, - or raise an error if it doesn't exist or backend is not well defined. + """Loads backend module. + + Returns database backend module given a fully qualified database + backend name, or raise an error if it doesn't exist or backend is not + well defined. """ try: assert 'BACKEND' in TIMESERIES_DB, 'BACKEND' diff --git a/openwisp_monitoring/db/backends/influxdb/client.py b/openwisp_monitoring/db/backends/influxdb/client.py index 906769a0..b6b9428d 100644 --- a/openwisp_monitoring/db/backends/influxdb/client.py +++ b/openwisp_monitoring/db/backends/influxdb/client.py @@ -62,21 +62,21 @@ def __init__(self, db_name=None): @retry def create_database(self): - """creates database if necessary""" + """Creates database if necessary.""" # InfluxDB does not create a new database, neither raise an error if database exists self.db.create_database(self.db_name) logger.debug(f'Created InfluxDB database "{self.db_name}"') @retry def drop_database(self): - """drops database if it exists""" + """Drops database if it exists.""" # InfluxDB does not raise an error if database does not exist self.db.drop_database(self.db_name) logger.debug(f'Dropped InfluxDB database "{self.db_name}"') @cached_property def db(self): - """Returns an ``InfluxDBClient`` instance""" + """Returns an ``InfluxDBClient`` instance.""" return self.dbs['default'] @cached_property @@ -123,7 +123,7 @@ def use_udp(self): @retry def create_or_alter_retention_policy(self, name, duration): - """creates or alters existing retention policy if necessary""" + """Creates or alters existing retention policy if necessary.""" retention_policies = self.db.get_list_retention_policies() exists = False duration_changed = False @@ -280,9 +280,10 @@ def get_list_retention_policies(self): return self.db.get_list_retention_policies() def delete_metric_data(self, key=None, tags=None): - """ - deletes a specific metric based on the key and tags - provided, you may also choose to delete all metrics + """Deletes a specific metric. + + Deletes a metric based on the key and tags provided, you may also + choose to delete all metrics. """ if not key and not tags: self.query('DROP SERIES FROM /.*/') @@ -393,10 +394,11 @@ def _group_by(self, query, time, chart_type, group_map, strip=False): ) def _fields(self, fields, query, field_name): - """ - support substitution of {fields||} - with (field1) AS field1 , - (field2) AS field2 + """Support substitution: + + of {fields||} with + (field1) AS field1 , + (field2) AS field2 """ matches = re.search(self._fields_regex, query) if not matches and not fields: diff --git a/openwisp_monitoring/device/admin.py b/openwisp_monitoring/device/admin.py index f24e0d05..3607a9d2 100644 --- a/openwisp_monitoring/device/admin.py +++ b/openwisp_monitoring/device/admin.py @@ -290,9 +290,7 @@ def get_object(self, request, object_id, from_field=None): return obj def get_form(self, request, obj=None, **kwargs): - """ - Adds the help_text of DeviceMonitoring.status field - """ + """Adds the help_text of DeviceMonitoring.status field""" health_status = DeviceMonitoring._meta.get_field('status').help_text kwargs.update( {'help_texts': {'health_status': health_status.replace('\n', '
')}} diff --git a/openwisp_monitoring/device/api/views.py b/openwisp_monitoring/device/api/views.py index 697318dc..2f0d7390 100644 --- a/openwisp_monitoring/device/api/views.py +++ b/openwisp_monitoring/device/api/views.py @@ -69,9 +69,7 @@ class ListViewPagination(pagination.PageNumberPagination): def get_device_args_rewrite(view, pk): - """ - Use only the PK parameter for calculating the cache key - """ + """Use only the PK parameter for calculating the cache key""" try: pk = uuid.UUID(pk) except ValueError: @@ -100,14 +98,15 @@ def get_authenticators(self): class DeviceMetricView( DeviceKeyAuthenticationMixin, MonitoringApiViewMixin, GenericAPIView ): - """ - Retrieve device information, monitoring status (health status), - a list of metrics, chart data and - optionally device status information (if ``?status=true``). + """Device Monitoring View. - Suports session authentication, token authentication, - or alternatively device key authentication passed as query - string parameter (this method is meant to be used by network devices). + Retrieve device information, monitoring status (health status), a list + of metrics, chart data and optionally device status information (if + ``?status=true``). + + Suports session authentication, token authentication, or alternatively + device key authentication passed as query string parameter (this + method is meant to be used by network devices). """ model = DeviceData @@ -125,9 +124,7 @@ class DeviceMetricView( @classmethod def invalidate_get_device_cache(cls, instance, **kwargs): - """ - Called from signal receiver which performs cache invalidation - """ + """Called from signal receiver which performs cache invalidation""" view = cls() view.get_object.invalidate(view, str(instance.pk)) logger.debug(f'invalidated view cache for device ID {instance.pk}') @@ -286,14 +283,13 @@ def get_queryset(self): class MonitoringDeviceList(DeviceListCreateView): - """ - Lists devices and their monitoring status (health status). + """Lists devices and their monitoring status (health status). Supports session authentication and token authentication. - `NOTE:` The response does not include the information and - health status of the specific metrics, this information - can be retrieved in the detail endpoint of each device. + `NOTE:` The response does not include the information and health + status of the specific metrics, this information can be retrieved in + the detail endpoint of each device. """ serializer_class = MonitoringDeviceListSerializer diff --git a/openwisp_monitoring/device/apps.py b/openwisp_monitoring/device/apps.py index 3fb18463..60c708f1 100644 --- a/openwisp_monitoring/device/apps.py +++ b/openwisp_monitoring/device/apps.py @@ -172,9 +172,11 @@ def device_recovery_detection(self): @classmethod def manage_device_recovery_cache_key(cls, instance, status, **kwargs): - """ - It sets the ``cache_key`` as 1 when device ``health_status`` goes to ``critical`` - and deletes the ``cache_key`` when device recovers from ``critical`` state + """Returns a cache key string. + + It sets the ``cache_key`` as 1 when device ``health_status`` goes + to ``critical`` and deletes the ``cache_key`` when device recovers + from ``critical`` state """ cache_key = get_device_cache_key(device=instance.device) if status == 'critical': diff --git a/openwisp_monitoring/device/base/models.py b/openwisp_monitoring/device/base/models.py index ec7b39b0..2f5e7a38 100644 --- a/openwisp_monitoring/device/base/models.py +++ b/openwisp_monitoring/device/base/models.py @@ -35,9 +35,9 @@ def mac_lookup_cache_timeout(): - """ - returns a random number of hours between 48 and 96 - this avoids timing out most of the cache at the same time + """Returns a random number of hours between 48 and 96. + + This avoids timing out the entire cache at the same time. """ return 60 * 60 * random.randint(48, 96) @@ -81,9 +81,7 @@ def invalidate_cache(cls, instance, *args, **kwargs): cls.get_devicedata.invalidate(cls, str(pk)) def can_be_updated(self): - """ - Do not attempt at pushing the conf if the device is not reachable - """ + """Do not attempt to push the conf if the device is not reachable.""" can_be_updated = super().can_be_updated() return can_be_updated and self.monitoring.status not in ['critical', 'unknown'] @@ -148,9 +146,7 @@ def data_user_friendly(self): @property def data(self): - """ - retrieves last data snapshot from Timeseries Database - """ + """Retrieves last data snapshot from Timeseries Database.""" if self.__data: return self.__data q = device_data_query.format(SHORT_RP, self.__key, self.pk) @@ -165,29 +161,21 @@ def data(self): @data.setter def data(self, data): - """ - sets data - """ + """Sets data.""" self.__data = data @property def data_timestamp(self): - """ - retrieves timestamp at which the data was recorded - """ + """Retrieves timestamp at which the data was recorded.""" return self.__data_timestamp @data_timestamp.setter def data_timestamp(self, value): - """ - sets the timestamp related to the data - """ + """Sets the timestamp related to the data.""" self.__data_timestamp = value def validate_data(self): - """ - validate data according to NetJSON DeviceMonitoring schema - """ + """Validates data according to NetJSON DeviceMonitoring schema.""" try: validate(self.data, self.schema, format_checker=draft7_format_checker) except SchemaError as e: @@ -199,9 +187,7 @@ def validate_data(self): raise ValidationError(message) def _transform_data(self): - """ - performs corrections or additions to the device data - """ + """Performs corrections or additions to the device data.""" mac_detection = app_settings.MAC_VENDOR_DETECTION for interface in self.data.get('interfaces', []): # loop over mobile signal values to convert them to float @@ -268,9 +254,7 @@ def _mac_lookup(self, value): return '' def save_data(self, time=None): - """ - validates and saves data to Timeseries Database - """ + """Validates and saves data to Timeseries Database.""" self.validate_data() self._transform_data() time = time or now() @@ -333,7 +317,10 @@ def save_wifi_clients_and_sessions(self): active_sessions.append(session_obj.pk) # Close open WifiSession - WifiSession.objects.filter(device_id=self.id, stop_time=None,).exclude( + WifiSession.objects.filter( + device_id=self.id, + stop_time=None, + ).exclude( pk__in=active_sessions ).update(stop_time=now()) @@ -395,9 +382,10 @@ def related_metrics(self): @staticmethod @receiver(threshold_crossed, dispatch_uid='threshold_crossed_receiver') def threshold_crossed(sender, metric, alert_settings, target, first_time, **kwargs): - """ - Changes the health status of a devicewhen a threshold defined in the - alert settings related to the metric is crossed. + """Executed when a threshold is crossed. + + Changes the health status of a devicewhen a threshold defined in + the alert settings related to the metric is crossed. """ DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring') if not isinstance(target, DeviceMonitoring.device.field.related_model): @@ -437,15 +425,15 @@ def is_metric_critical(metric): @classmethod def handle_disabled_organization(cls, organization_id): - """ - Clears the management IP of all devices belonging to a - disabled organization and set their monitoring status to 'unknown'. + """Handles the disabling of an organization. + + Clears the management IP of all devices belonging to a disabled + organization and set their monitoring status to 'unknown'. - Parameters: - organization_id (int): The ID of the disabled organization. + Parameters: - organization_id (int): The ID of the disabled + organization. - Returns: - None + Returns: - None """ load_model('config', 'Device').objects.filter( organization_id=organization_id diff --git a/openwisp_monitoring/device/migrations/0001_squashed_0002_devicemonitoring.py b/openwisp_monitoring/device/migrations/0001_squashed_0002_devicemonitoring.py index b2143256..34b6efd1 100644 --- a/openwisp_monitoring/device/migrations/0001_squashed_0002_devicemonitoring.py +++ b/openwisp_monitoring/device/migrations/0001_squashed_0002_devicemonitoring.py @@ -15,9 +15,7 @@ def create_device_monitoring(apps, schema_editor): - """ - Data migration - """ + """Data migration""" Device = apps.get_model('config', 'Device') DeviceMonitoring = apps.get_model('device_monitoring', 'DeviceMonitoring') for device in Device.objects.all(): diff --git a/openwisp_monitoring/device/tasks.py b/openwisp_monitoring/device/tasks.py index 03ab8577..94efe24b 100644 --- a/openwisp_monitoring/device/tasks.py +++ b/openwisp_monitoring/device/tasks.py @@ -14,11 +14,13 @@ @shared_task(base=OpenwispCeleryTask) def trigger_device_checks(pk, recovery=True): - """ - Retrieves all related checks to the passed ``device`` - and calls the ``perform_check`` task from each of them. - If no check exists changes the status according to the - ``recovery`` argument. + """Triggers the monitoring checks for the specified device pk. + + Retrieves all related checks to the passed ``device`` and calls the + ``perform_check`` task from each of them. + + If no check exists changes the status according to the ``recovery`` + argument. """ DeviceData = load_model('device_monitoring', 'DeviceData') try: diff --git a/openwisp_monitoring/device/tests/__init__.py b/openwisp_monitoring/device/tests/__init__.py index b0d58011..98af8830 100644 --- a/openwisp_monitoring/device/tests/__init__.py +++ b/openwisp_monitoring/device/tests/__init__.py @@ -78,10 +78,10 @@ def _transform_wireless_interface_test_data(self, data): return data def assertDataDict(self, dd_data, data): - """ - This method is necessary as the wireless interface data - is modified by the `AbstractDeviceData._transform_data` - method. + """Compares monitoring data. + + This method is necessary because the wireless interface data is + modified by the `AbstractDeviceData._transform_data` method. """ data = self._transform_wireless_interface_test_data(data) self.assertDictEqual(dd_data, data) diff --git a/openwisp_monitoring/device/tests/test_admin.py b/openwisp_monitoring/device/tests/test_admin.py index 8c7a9e32..45c3b3b5 100644 --- a/openwisp_monitoring/device/tests/test_admin.py +++ b/openwisp_monitoring/device/tests/test_admin.py @@ -41,9 +41,7 @@ class TestAdmin( TestWifiClientSessionMixin, TestImportExportMixin, DeviceMonitoringTestCase ): - """ - Test the additions of openwisp-monitoring to DeviceAdmin - """ + """Test the additions of openwisp-monitoring to DeviceAdmin""" resources_fields = TestImportExportMixin.resource_fields resources_fields.append('monitoring_status') @@ -1093,9 +1091,11 @@ def test_wifi_client_he_vht_ht_unknown(self): """.format( # TODO: Remove this when dropping support for Django 3.2 and 4.0 - start_div='

' - if django.VERSION >= (4, 2) - else '', + start_div=( + '
' + if django.VERSION >= (4, 2) + else '' + ), end_div='
' if django.VERSION >= (4, 2) else '', ), html=True, diff --git a/openwisp_monitoring/device/tests/test_api.py b/openwisp_monitoring/device/tests/test_api.py index db369f70..9a56d314 100644 --- a/openwisp_monitoring/device/tests/test_api.py +++ b/openwisp_monitoring/device/tests/test_api.py @@ -39,9 +39,7 @@ class TestDeviceApi(AuthenticationMixin, TestGeoMixin, DeviceMonitoringTestCase): - """ - Tests API (device metric collection) - """ + """Tests API (device metric collection).""" location_model = Location object_location_model = DeviceLocation diff --git a/openwisp_monitoring/device/tests/test_models.py b/openwisp_monitoring/device/tests/test_models.py index e539c070..66ec6932 100644 --- a/openwisp_monitoring/device/tests/test_models.py +++ b/openwisp_monitoring/device/tests/test_models.py @@ -247,9 +247,7 @@ def _create_device_data(self, **kwargs): class TestDeviceData(BaseTestCase): - """ - Test openwisp_monitoring.device.models.DeviceData - """ + """Test openwisp_monitoring.device.models.DeviceData""" def test_clean_data_ok(self): dd = self._create_device_data() @@ -548,9 +546,7 @@ def test_calculate_increment(self): class TestDeviceMonitoring(CreateConnectionsMixin, BaseTestCase): - """ - Test openwisp_monitoring.device.models.DeviceMonitoring - """ + """Test openwisp_monitoring.device.models.DeviceMonitoring""" def _create_env(self): d = self._create_device() diff --git a/openwisp_monitoring/device/tests/test_recovery.py b/openwisp_monitoring/device/tests/test_recovery.py index 0861ae38..cf3553f5 100644 --- a/openwisp_monitoring/device/tests/test_recovery.py +++ b/openwisp_monitoring/device/tests/test_recovery.py @@ -15,9 +15,7 @@ class TestRecovery(DeviceMonitoringTestCase): - """ - Tests ``Device Recovery Detection`` functionality - """ + """Tests ``Device Recovery Detection`` functionality""" def test_device_recovery_cache_key_not_set(self): device_monitoring_app = DeviceMonitoring._meta.app_config @@ -56,9 +54,7 @@ def test_device_recovery_cache_key_set(self): self.assertEqual(cache.get(cache_key), None) def test_status_set_ok(self): - """ - Tests device status is set to ok if no related checks present - """ + """Tests device status is set to ok if no related checks present""" dm = self._create_device_monitoring() dm.update_status('critical') trigger_device_checks.delay(dm.device.pk) @@ -66,10 +62,7 @@ def test_status_set_ok(self): self.assertEqual(dm.status, 'ok') def test_status_set_critical(self): - """ - Tests device status is set to critical if no related checks present - and recovery=False is passed - """ + """Tests device status is set to critical if no related checks present and recovery=False is passed""" dm = self._create_device_monitoring() dm.update_status('critical') trigger_device_checks.delay(dm.device.pk, recovery=False) diff --git a/openwisp_monitoring/device/tests/test_settings.py b/openwisp_monitoring/device/tests/test_settings.py index 44725310..6ef73610 100644 --- a/openwisp_monitoring/device/tests/test_settings.py +++ b/openwisp_monitoring/device/tests/test_settings.py @@ -6,9 +6,7 @@ class TestSettings(DeviceMonitoringTestCase): - """ - Tests ``OpenWISP Device settings`` functionality - """ + """Tests ``OpenWISP Device settings`` functionality""" @patch( 'django.conf.settings.OPENWISP_MONITORING_CRITICAL_DEVICE_METRICS', diff --git a/openwisp_monitoring/device/utils.py b/openwisp_monitoring/device/utils.py index 151b6260..931cd991 100644 --- a/openwisp_monitoring/device/utils.py +++ b/openwisp_monitoring/device/utils.py @@ -10,16 +10,12 @@ def get_device_cache_key(device, context='react-to-updates'): def manage_short_retention_policy(): - """ - creates or updates the "short" retention policy - """ + """creates or updates the "short" retention policy""" duration = app_settings.SHORT_RETENTION_POLICY timeseries_db.create_or_alter_retention_policy(SHORT_RP, duration) def manage_default_retention_policy(): - """ - creates or updates the "default" retention policy - """ + """creates or updates the "default" retention policy""" duration = app_settings.DEFAULT_RETENTION_POLICY timeseries_db.create_or_alter_retention_policy(DEFAULT_RP, duration) diff --git a/openwisp_monitoring/device/writer.py b/openwisp_monitoring/device/writer.py index 8b0fdd7a..48071a4c 100644 --- a/openwisp_monitoring/device/writer.py +++ b/openwisp_monitoring/device/writer.py @@ -19,21 +19,18 @@ class DeviceDataWriter(object): - """ - This class is in charge of writing the device metric data. - Before these methods were shipped in the REST API view - but later have been moved here to allow writing this data in - the background processes of OpenWISP. + """This class is in charge of writing the device metric data. + + Before these methods were shipped in the REST API view but later have + been moved here to allow writing this data in the background processes + of OpenWISP. """ def __init__(self, device_data): self.device_data = device_data def _init_previous_data(self): - """ - makes NetJSON interfaces of previous - snapshots more easy to access - """ + """makes NetJSON interfaces of previous snapshots more easy to access""" data = self.device_data.data or {} if data: data = deepcopy(data) @@ -45,9 +42,10 @@ def _init_previous_data(self): def _append_metric_data( self, metric, value, current=False, time=None, extra_values=None ): - """ - Appends to the data structure which holds metric data - and which will be sent to the timeseries DB. + """Appends data for writing. + + Appends to the data structure which holds metric data and which + will be sent to the timeseries DB. """ self.write_device_metrics.append( ( @@ -340,10 +338,7 @@ def _write_memory( ) def _calculate_increment(self, ifname, stat, value): - """ - compares value with previously stored counter and - calculates the increment of the value (which is returned) - """ + """Returns how much a counter has incremented since its last saved value.""" # get previous counters data = self._previous_data try: @@ -364,9 +359,7 @@ def _calculate_increment(self, ifname, stat, value): return int(value) def _create_traffic_chart(self, metric): - """ - create "traffic (GB)" chart - """ + """Creates "traffic (GB)" chart.""" if 'traffic' not in monitoring_settings.AUTO_CHARTS: return chart = Chart(metric=metric, configuration='traffic') @@ -374,9 +367,7 @@ def _create_traffic_chart(self, metric): chart.save() def _create_clients_chart(self, metric): - """ - creates "WiFi associations" chart - """ + """Creates "WiFi associations" chart.""" if 'wifi_clients' not in monitoring_settings.AUTO_CHARTS: return chart = Chart(metric=metric, configuration='wifi_clients') diff --git a/openwisp_monitoring/monitoring/api/views.py b/openwisp_monitoring/monitoring/api/views.py index 83f66be7..e597a38f 100644 --- a/openwisp_monitoring/monitoring/api/views.py +++ b/openwisp_monitoring/monitoring/api/views.py @@ -19,9 +19,7 @@ class DashboardTimeseriesView(ProtectedAPIMixin, MonitoringApiViewMixin, APIView): - """ - Multi-tenant view that returns general monitoring - charts for the admin dashboard. + """Multi-tenant view that returns general monitoring charts for the admin dashboard. Allows filtering with organization slugs. """ @@ -167,10 +165,7 @@ def invalidate_cache(cls, instance, *args, **kwargs): cls._get_charts.invalidate() def _get_user_managed_orgs(self, request): - """ - Return list of dictionary containing organization name and slug - in select2 compatible format. - """ + """Return list of dictionary containing organization name and slug in select2 compatible format.""" orgs = [] qs = Organization.objects.only('slug', 'name') if not request.user.is_superuser: diff --git a/openwisp_monitoring/monitoring/base/models.py b/openwisp_monitoring/monitoring/base/models.py index 5d7bf0eb..743025e1 100644 --- a/openwisp_monitoring/monitoring/base/models.py +++ b/openwisp_monitoring/monitoring/base/models.py @@ -161,9 +161,10 @@ def _get_or_create( cls, **kwargs, ): - """ - like ``get_or_create`` method of django model managers - but with validation before creation + """Gets or creates a metric. + + Like ``get_or_create`` method of django model managers but with + validation before creation. """ if 'key' in kwargs: kwargs['key'] = cls._makekey(kwargs['key']) @@ -215,7 +216,7 @@ def invalidate_cache(cls, instance, *args, **kwargs): @property def codename(self): - """identifier stored in timeseries db""" + """Identifier stored in timeseries db.""" return self._makekey(self.name) @property @@ -234,7 +235,10 @@ def related_fields(self): # TODO: This method needs to be refactored when adding the other db @staticmethod def _makekey(value): - """makes value suited for InfluxDB key""" + """Produces a valid InfluxDB key from ``value``. + + Takes ``value`` as input argument and returs a valid InfluxDB key. + """ return clean_timeseries_data_key(value) @property @@ -255,9 +259,7 @@ def tags(self): @staticmethod def _sort_dict(dict_): - """ - ensures the order of the keys in the dict not random - """ + """Ensures the order of the keys in the dict is predictable.""" if not isinstance(dict_, OrderedDict): return OrderedDict(sorted(dict_.items())) return dict_ @@ -281,23 +283,20 @@ def alert_on_related_field(self): return self.alert_field in self.related_fields def _get_time(self, time): - """ - If time is a string, convert it to a datetime - """ + """If time is a string, it converts it to a datetime.""" if isinstance(time, str): return parse_date(time) return time def _set_is_healthy(self, alert_settings, value): - """ - Sets the value of "is_healthy" field if "value" - crosses threshold defined in "alert_settings". - Returns "True" if "is_healthy" field is changed. + """Sets the value of "is_healthy" field when necessary. + + Executes only if "value" crosses the threshold defined in + "alert_settings". Returns "True" if "is_healthy" field is changed. Otherwise, returns "None". - This method does not take into account the alert - settings tolerance, which is done by - "_set_is_healthy_tolerant" method. + This method does not take into account the alert settings + tolerance, which is done by "_set_is_healthy_tolerant" method. """ crossed = alert_settings._value_crossed(value) if (not crossed and self.is_healthy) or (crossed and self.is_healthy is False): @@ -313,14 +312,14 @@ def _set_is_healthy(self, alert_settings, value): def _set_is_healthy_tolerant( self, alert_settings, value, time, retention_policy, send_alert ): - """ - Sets the value of "is_tolerance_healthy" if "value" - crosses the threshold for more than the amount of seconds - defined in the alert_settings "tolerance" field. - It also sends the notification if required. - Returns "None" if value of "is_healthy_tolerant" is unchanged. - Returns "True" if it is the first metric write within threshold. - Returns "False" in other cases. + """Sets the value of "is_tolerance_healthy" if necessary. + + Executes only if "value" crosses the threshold for more than the + amount of seconds defined in the alert_settings "tolerance" field. + It also sends the notification if required. Returns "None" if + value of "is_healthy_tolerant" is unchanged. Returns "True" if it + is the first metric write within threshold. Returns "False" in + other cases. This method is similar to "_set_is_healthy" but it takes into account the alert settings tolerance so it's slightly different @@ -364,9 +363,7 @@ def _set_is_healthy_tolerant( return first_time def check_threshold(self, value, time=None, retention_policy=None, send_alert=True): - """ - Checks if the threshold is crossed and notifies users accordingly - """ + """Checks if the threshold is crossed and notifies users accordingly""" try: alert_settings = self.alertsettings except ObjectDoesNotExist: @@ -624,9 +621,10 @@ def _default_query(self): @classmethod def _get_group_map(cls, time=None): - """ - Returns the chart group map for the specified days, - otherwise the default Chart.GROUP_MAP is returned + """Returns group map. + + Returns the chart group map for the specified days, otherwise the + default Chart.GROUP_MAP is returned. """ if ( not time @@ -689,7 +687,8 @@ def get_query( ) def get_top_fields(self, number): - """ + """Returns the top fields. + Returns list of top ``number`` of fields (highest sum) of a measurement in the specified time range (descending order). """ @@ -826,9 +825,7 @@ def json(self, time=DEFAULT_TIME, **kwargs): @staticmethod def _round(value, decimal_places): - """ - rounds value if it makes sense - """ + """Rounds value when necessary.""" control = 1.0 / 10**decimal_places if value < control: decimal_places += 2 @@ -933,10 +930,10 @@ def _time_crossed(self, time): @property def _tolerance_search_range(self): - """ - Allow sufficient room for checking - if the tolerance has been trepassed. - Minimum 15 minutes, maximum self._MINUTES_MAX * 1.05 + """Returns an amount of minutes to search. + + Allows sufficient room for checking if the tolerance has been + trepassed. Minimum 15 minutes, maximum self._MINUTES_MAX * 1.05. """ minutes = self.tolerance * 2 minutes = minutes if minutes > 15 else 15 @@ -944,8 +941,10 @@ def _tolerance_search_range(self): return int(minutes) def _is_crossed_by(self, current_value, time=None, retention_policy=None): - """ - do current_value and time cross the threshold and trepass the tolerance? + """Answers the following question: + + do current_value and time cross the threshold and trepass the + tolerance? """ value_crossed = self._value_crossed(current_value) if value_crossed is NotImplemented: diff --git a/openwisp_monitoring/monitoring/configuration.py b/openwisp_monitoring/monitoring/configuration.py index 9f0de2ef..d3d5159c 100644 --- a/openwisp_monitoring/monitoring/configuration.py +++ b/openwisp_monitoring/monitoring/configuration.py @@ -743,9 +743,7 @@ def get_metric_configuration_choices(): def register_metric(metric_name, metric_config): - """ - Registers a new metric configuration. - """ + """Registers a new metric configuration.""" if not isinstance(metric_name, str): raise ImproperlyConfigured('Metric configuration name should be type "str".') if not isinstance(metric_config, dict): @@ -817,9 +815,7 @@ def get_chart_configuration_choices(): def register_chart(chart_name, chart_config): - """ - Registers a new chart configuration. - """ + """Registers a new chart configuration.""" if not isinstance(chart_name, str): raise ImproperlyConfigured('Chart name should be type "str".') if not isinstance(chart_config, dict): diff --git a/openwisp_monitoring/monitoring/migrations/influxdb/influxdb_alter_structure_0006.py b/openwisp_monitoring/monitoring/migrations/influxdb/influxdb_alter_structure_0006.py index 406bc4ea..af437262 100644 --- a/openwisp_monitoring/monitoring/migrations/influxdb/influxdb_alter_structure_0006.py +++ b/openwisp_monitoring/monitoring/migrations/influxdb/influxdb_alter_structure_0006.py @@ -40,9 +40,7 @@ def get_writable_data(read_data, tags, old_measurement, new_measurement): - """ - Prepares data that can be written by "timeseries_db.db.write_points" - """ + """Prepares data that can be written by "timeseries_db.db.write_points".""" write_data = [] for data_point in read_data.get_points(measurement=old_measurement): data = { @@ -171,11 +169,11 @@ def migrate_traffic_data(): def requires_migration(): - """ - Returns "False" if all measurements presents in InfluxDB - are present in EXCLUDED_MEASUREMENTS. This means that there - are no interface specific measurements. - Otherwise, returns "True". + """Indicates whether influxdb data migration is necessary. + + Returns "False" if all measurements presents in InfluxDB are present + in EXCLUDED_MEASUREMENTS. This means that there are no interface + specific measurements. Otherwise, returns "True". """ tsdb_measurements = timeseries_db.db.get_list_measurements() for measurement in tsdb_measurements: diff --git a/openwisp_monitoring/monitoring/tasks.py b/openwisp_monitoring/monitoring/tasks.py index 392cb674..9778b8ff 100644 --- a/openwisp_monitoring/monitoring/tasks.py +++ b/openwisp_monitoring/monitoring/tasks.py @@ -42,18 +42,13 @@ def _metric_post_write(name, values, metric, check_threshold_kwargs, **kwargs): def timeseries_write( self, name, values, metric=None, check_threshold_kwargs=None, **kwargs ): - """ - write with exponential backoff on a failure - """ + """Writes and retries with exponential backoff on failures.""" timeseries_db.write(name, values, **kwargs) _metric_post_write(name, values, metric, check_threshold_kwargs, **kwargs) def _timeseries_write(name, values, metric=None, check_threshold_kwargs=None, **kwargs): - """ - If the timeseries database is using UDP to write data, - then write data synchronously. - """ + """Handles writes synchronously when using UDP mode.""" if timeseries_db.use_udp: func = timeseries_write else: @@ -75,9 +70,10 @@ def _timeseries_write(name, values, metric=None, check_threshold_kwargs=None, ** **RETRY_OPTIONS ) def timeseries_batch_write(self, data): - """ - Similar to timeseries_write function above, but operates on - list of metric data (batch operation) + """Writes data in batches. + + Similar to timeseries_write function above, but operates on list of + metric data (batch operation) """ timeseries_db.batch_write(data) for metric_data in data: @@ -85,10 +81,7 @@ def timeseries_batch_write(self, data): def _timeseries_batch_write(data): - """ - If the timeseries database is using UDP to write data, - then write data synchronously. - """ + """If the timeseries database is using UDP to write data, then write data synchronously.""" if timeseries_db.use_udp: timeseries_batch_write(data=data) else: @@ -104,12 +97,12 @@ def delete_timeseries(key, tags): @shared_task def migrate_timeseries_database(): - """ - Perform migrations on timeseries database - asynchronously for changes introduced in + """Performs migrations of timeseries datab. + + Performed asynchronously, due to changes introduced in https://github.com/openwisp/openwisp-monitoring/pull/368 - To be removed in 1.1.0 release. + To be removed in a future release. """ from .migrations.influxdb.influxdb_alter_structure_0006 import ( migrate_influxdb_structure, diff --git a/openwisp_monitoring/monitoring/tests/test_charts.py b/openwisp_monitoring/monitoring/tests/test_charts.py index 23680ee4..d5fe9cb5 100644 --- a/openwisp_monitoring/monitoring/tests/test_charts.py +++ b/openwisp_monitoring/monitoring/tests/test_charts.py @@ -24,9 +24,7 @@ class TestCharts(TestMonitoringMixin, TestCase): - """ - Tests for functionalities related to charts - """ + """Tests for functionalities related to charts""" def test_read(self): c = self._create_chart() diff --git a/openwisp_monitoring/monitoring/tests/test_monitoring_notifications.py b/openwisp_monitoring/monitoring/tests/test_monitoring_notifications.py index 65580d03..d183e550 100644 --- a/openwisp_monitoring/monitoring/tests/test_monitoring_notifications.py +++ b/openwisp_monitoring/monitoring/tests/test_monitoring_notifications.py @@ -108,12 +108,12 @@ def test_resources_metric_threshold_deferred_not_crossed(self): self.assertEqual(Notification.objects.count(), 0) def test_general_check_threshold_crossed_for_long_time(self): - """ - this is going to be the most realistic scenario: - incoming metrics will always be stored with the current - timestamp, which means the system must be able to look - back in previous measurements to see if the AlertSettings - has been crossed for long enough + """Test threshold remains crossed for a long time. + + This is the most common scenario: incoming metrics will always be + stored with the current timestamp, which means the system must be + able to look back in previous measurements to see if the + AlertSettings has been crossed for long enough. """ admin = self._create_admin() m = self._create_general_metric(name='load') diff --git a/openwisp_monitoring/views.py b/openwisp_monitoring/views.py index 840042a5..3be0c5e1 100644 --- a/openwisp_monitoring/views.py +++ b/openwisp_monitoring/views.py @@ -22,16 +22,11 @@ class MonitoringApiViewMixin: def _get_charts(self, request, *args, **kwargs): - """ - Hook to return Chart query. - """ + """Hook to return Chart query.""" raise NotImplementedError def _get_additional_data(request, *args, **kwargs): - """ - Hook to return any additonal data that should be - included in the response. - """ + """Hook to return any additonal data that should be included in the response.""" return {} def _validate_custom_date(self, start, end, tmz): @@ -88,9 +83,7 @@ def get(self, request, *args, **kwargs): return Response(data) def _get_chart_additional_query_kwargs(self, chart): - """ - Hook to provide additional kwargs to Chart.read. - """ + """Hook to provide additional kwargs to Chart.read.""" return None def _get_charts_data(self, charts, time, timezone, start_date, end_date): diff --git a/setup.py b/setup.py index 43ca4bb9..e0f84b79 100755 --- a/setup.py +++ b/setup.py @@ -8,9 +8,7 @@ def get_install_requires(): - """ - parse requirements.txt, ignore links, exclude comments - """ + """parse requirements.txt, ignore links, exclude comments""" requirements = [] for line in open('requirements.txt').readlines(): # skip to next iteration if comment or empty line diff --git a/tests/openwisp2/sample_check/models.py b/tests/openwisp2/sample_check/models.py index 10ad15a0..b39291ca 100644 --- a/tests/openwisp2/sample_check/models.py +++ b/tests/openwisp2/sample_check/models.py @@ -12,9 +12,10 @@ class Meta(AbstractCheck.Meta): abstract = False def perform_check(self, store=True): - """ - This method has been added for testing `last_called` field. - It need not be added to retain original behaviour. + """Performs check. + + This method has been added for testing `last_called` field. It + need not be added to retain original behaviour. """ self.last_called = now() self.full_clean() From 34943a342f3a00d59a4f5df6901254367cd30c53 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 1 Aug 2024 19:28:15 -0400 Subject: [PATCH 41/42] [docs] Fixed contributing --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d34f22c7..9602293b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,2 +1,2 @@ Please refer to the `OpenWISP Contributing Guidelines -`_. \ No newline at end of file +`_. From 41cd261cc40a52df0ac38a63f02594d695773ccb Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Aug 2024 11:18:55 -0400 Subject: [PATCH 42/42] [qa] Don't skip README in docstrfmt --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 424b6800..ef07e60d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ omit = [ ] [tool.docstrfmt] -extend_exclude = ["**/*.py", "README.rst"] +extend_exclude = ["**/*.py"] [tool.isort] known_third_party = ["django", "django_x509"]