From f98f2660e644a6ae7022addaf9ab3a042756f667 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 21 Feb 2024 19:41:04 +0530 Subject: [PATCH 01/44] [docs] Restructured docs --- README.rst | 4273 ----------------- docs/developer/developer-docs.rst | 12 + docs/developer/extending.rst | 749 +++ docs/developer/installation.rst | 176 + docs/developer/project-structure.rst | 96 + docs/developer/signals.rst | 239 + docs/overview.rst | 59 + .../automatic-provisioning-of-subnets.rst | 141 + docs/user/device-groups.rst | 103 + docs/user/how-to-configure-push-updates.rst | 92 + .../how-to-setup-vxlan-over-wireguard.rst | 102 + docs/user/how-to-setup-wireguard.rst | 101 + docs/user/notification-alerts.rst | 10 + docs/user/organization-limits.rst | 11 + docs/user/rest-api.rst | 1130 +++++ docs/user/send-commands.rst | 192 + docs/user/settings.rst | 698 +++ docs/user/templates-and-variables.rst | 193 + docs/user/zerotier.rst | 108 + 19 files changed, 4212 insertions(+), 4273 deletions(-) create mode 100644 docs/developer/developer-docs.rst create mode 100644 docs/developer/extending.rst create mode 100644 docs/developer/installation.rst create mode 100644 docs/developer/project-structure.rst create mode 100644 docs/developer/signals.rst create mode 100644 docs/overview.rst create mode 100644 docs/user/automatic-provisioning-of-subnets.rst create mode 100644 docs/user/device-groups.rst create mode 100644 docs/user/how-to-configure-push-updates.rst create mode 100644 docs/user/how-to-setup-vxlan-over-wireguard.rst create mode 100644 docs/user/how-to-setup-wireguard.rst create mode 100644 docs/user/notification-alerts.rst create mode 100644 docs/user/organization-limits.rst create mode 100644 docs/user/rest-api.rst create mode 100644 docs/user/send-commands.rst create mode 100644 docs/user/settings.rst create mode 100644 docs/user/templates-and-variables.rst create mode 100644 docs/user/zerotier.rst diff --git a/README.rst b/README.rst index 155094a6c..17aa278fa 100644 --- a/README.rst +++ b/README.rst @@ -86,4279 +86,6 @@ see the ------------ -Project Structure & main features ----------------------------------- - -OpenWISP Controller is a python package consisting of four django apps: - -Config App -~~~~~~~~~~ - -* **configuration management** for embedded devices supporting different firmwares: - - `OpenWRT `_ - - `OpenWISP Firmware `_ - - support for additional firmware can be added by `specifying custom backends <#netjsonconfig-backends>`_ -* **configuration editor** based on `JSON-Schema editor `_ -* **advanced edit mode**: edit `NetJSON `_ *DeviceConfiguration* objects for maximum flexibility -* `configuration templates `_: - reduce repetition to the minimum, configure default and required templates -* `configuration variables <#how-to-use-configuration-variables>`_: - reference ansible-like variables in the configuration and templates -* **template tags**: tag templates to automate different types of auto-configurations (eg: mesh, WDS, 4G) -* **device groups**: add `devices to dedicated groups <#device-groups>`_ to - ease management of group of devices -* **simple HTTP resources**: allow devices to automatically download configuration updates -* **VPN management**: `automatically provision VPN tunnels <#openwisp-controller-default-auto-cert>`_, - including cryptographic keys, IP addresses -* `REST API <#rest-api-reference>`_ -* `Export/Import devices <#exportimport-device-data>`_ - -PKI App -~~~~~~~ - -The PKI app is based on `django-x509 `_, -it allows to create, import and view x509 CAs and certificates directly from -the administration dashboard, it also adds different endpoints to the -`REST API <#rest-api-reference>`_. - -Connection App -~~~~~~~~~~~~~~ - -This app allows OpenWISP Controller to use different protocols to reach network devices. -Currently, the default connnection protocols are SSH and SNMP, but the protocol -mechanism is extensible and more protocols can be implemented if needed. - -SSH -### - -The SSH connector allows the controller to initialize connections to the devices -in order perform `push operations <#how-to-configure-push-updates>`__: - -- Sending configuration updates. -- `Executing shell commands <#sending-commands-to-devices>`_. -- Perform `firmware upgrades via the additional firmware upgrade module `_. -- `REST API <#rest-api-reference>`_ - -The default connection protocol implemented is SSH, but other protocol -mechanism is extensible and custom protocols can be implemented as well. - -Access via SSH key is recommended, the SSH key algorithms supported are: - -- RSA -- Ed25519 - -SNMP -#### - -The SNMP connector is useful to collect monitoring information and it's used in -`openwisp-monitoring`_ for performing checks to collect monitoring information. -`Read more `_ on how to use it. - -Geo App -~~~~~~~ - -The geographic app is based on `django-loci `_ -and allows to define the geographic coordinates of the devices, -as well as their indoor coordinates on floorplan images. - -It also adds different endpoints to the `REST API <#rest-api-reference>`_. - -Subnet Division App -~~~~~~~~~~~~~~~~~~~ - -This app allows to automatically provision subnets and IP addresses which will be -available as `system defined configuration variables <#system-defined-variables>`_ -that can be used in templates. The purpose of this app is to allow users to automatically -provision and configure specific -subnets and IP addresses to the devices without the need of manual intervention. - -Refer to `"How to configure automatic provisioning of subnets and IPs" -section of this documentation -<#how-to-configure-automatic-provisioning-of-subnets-and-ips>`_ -to learn about features provided by this app. - -This app is optional, if you don't need it you can avoid adding it to -``settings.INSTALLED_APPS``. - -Installation instructions -------------------------- - -Deploy it in production -~~~~~~~~~~~~~~~~~~~~~~~ - -See: - -- `ansible-openwisp2 `_ -- `docker-openwisp `_ - -Dependencies -~~~~~~~~~~~~ - -* Python >= 3.7 -* OpenSSL - -Install stable version from pypi -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install from pypi: - -.. code-block:: shell - - pip install openwisp-controller - -Install development version -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install tarball: - -.. code-block:: shell - - pip install https://github.com/openwisp/openwisp-controller/tarball/master - -Alternatively you can install via pip using git: - -.. code-block:: shell - - pip install -e git+git://github.com/openwisp/openwisp-controller#egg=openwisp_controller - -If you want to contribute, follow the instructions in -`Installing for development <#installing-for-development>`_. - -Installing for development -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install the system dependencies: - -.. code-block:: shell - - 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 chromium - -Fork and clone the forked repository: - -.. code-block:: shell - - git clone git://github.com//openwisp-controller - -Navigate into the cloned repository: - -.. code-block:: shell - - cd openwisp-controller/ - -Launch Redis and PostgreSQL: - -.. code-block:: shell - - docker-compose up -d redis postgres - -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 - -Launch celery worker (for background jobs): - -.. code-block:: shell - - celery -A openwisp2 worker -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 --parallel - # To run database tests against PostgreSQL backend - POSTGRESQL=1 ./runtests.py --parallel - -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 - -Troubleshooting steps for common installation issues -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You may encounter some issues while installing GeoDjango. - -Unable to load SpatiaLite library extension? -############################################ - -If you are getting below exception:: - - django.core.exceptions.ImproperlyConfigured: Unable to load the SpatiaLite library extension - -then, You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as explained in -`django documentation regarding how to install and configure spatialte -`_. - -Having Issues with other geospatial libraries? -############################################## - -Please refer -`troubleshooting issues related to geospatial libraries -`_. - -Setup (integrate in an existing django project) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add ``openwisp_controller`` applications to ``INSTALLED_APPS``: - -.. code-block:: python - - INSTALLED_APPS = [ - ... - # openwisp2 modules - 'openwisp_controller.config', - 'openwisp_controller.pki', - 'openwisp_controller.geo', - 'openwisp_controller.connection', - 'openwisp_controller.subnet_division', # Optional - 'openwisp_controller.notifications', - 'openwisp_users', - 'openwisp_notifications', - 'openwisp_ipam', - # openwisp2 admin theme - # (must be loaded here) - 'openwisp_utils.admin_theme', - 'admin_auto_filters', - 'django.contrib.admin', - 'django.forms', - 'import_export', - ... - ] - EXTENDED_APPS = ('django_x509', 'django_loci') - -**Note**: The order of applications in ``INSTALLED_APPS`` should be maintained, -otherwise it might not work properly. - -Other settings needed in ``settings.py``: - -.. code-block:: python - - STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'openwisp_utils.staticfiles.DependencyFinder', - ] - - ASGI_APPLICATION = 'openwisp_controller.geo.channels.routing.channel_routing' - CHANNEL_LAYERS = { - # in production you should use another channel layer backend - 'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}, - } - - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - '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', - 'openwisp_utils.admin_theme.context_processor.menu_items', - 'openwisp_notifications.context_processors.notification_api_settings', - ], - }, - } - ] - - FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' - -Add the URLs to your main ``urls.py``: - -.. code-block:: python - - urlpatterns = [ - # ... other urls in your project ... - # openwisp-controller urls - url(r'^admin/', admin.site.urls), - url(r'', include('openwisp_controller.urls')), - ] - -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' - - INSTALLED_APPS.append('djcelery_email') - EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' - -If you decide to use redis (as shown in these examples), -install the required python packages:: - - pip install redis django-redis - -Then run: - -.. code-block:: shell - - ./manage.py migrate - -Usage reference ---------------- - -Default Templates -~~~~~~~~~~~~~~~~~ - -When templates are flagged as default, they will be automatically assigned to new devices. - -If there are multiple default templates, these are assigned to the device in alphabetical -order based on their names, for example, given the following default templates: - -- Access -- Interfaces -- SSH Keys - -They will be assigned to devices in exactly that order. - -If for some technical reason (eg: one default template depends on the presence of another -default template which must be assigned earlier) you need to change the ordering, you can -simply rename the templates by prefixing them with numbers, eg: - -- 1 Interfaces -- 2. SSH Keys -- 3. Access - -Required Templates -~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png - :alt: Required template example - -Required templates are similar to `Default templates <#default-templates>`__ -but cannot be unassigned from a device configuration, they can only be overridden. - -They will be always assigned earlier than default templates, -so they can be overridden if needed. - -In the example above, the "SSID" template is flagged as "(required)" -and its checkbox is always checked and disabled. - -How to use configuration variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Sometimes the configuration is not exactly equal on all the devices, -some parameters are unique to each device or need to be changed -by the user. - -In these cases it is possible to use configuration variables in conjunction -with templates, this feature is also known as *configuration context*, think of -it like a dictionary which is passed to the function which renders the -configuration, so that it can fill variables according to the passed context. - -The different ways in which variables are defined are described below in -the order (high to low) of their precedence: - -1. `User defined device variables <#user-defined-device-variables>`_ -2. `Predefined device variables <#predefined-device-variables>`_ -3. `Group variables <#group-variables>`_ -4. `Organization variables <#organization-variables>`_ -5. `Global variables <#global-variables>`_ -6. `Template default values <#template-default-values>`_ - -User defined device variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the device configuration section you can find a section named -"Configuration variables" where it is possible to define the configuration -variables and their values, as shown in the example below: - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png - :alt: context - -Predefined device variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Each device gets the following attributes passed as configuration variables: - -* ``id`` -* ``key`` -* ``name`` -* ``mac_address`` - -Group variables -~~~~~~~~~~~~~~~ - -Variables can also be defined in `Device groups <#device-groups>`__. - -Refer the `Group configuration variables `_ -section for detailed information. - -Organization variables -~~~~~~~~~~~~~~~~~~~~~~ - -Variables can also be defined at the organization level. - -You can set the *organization variables* from the organization change page -``/admin/openwisp_users/organization//change/``, under the -**Configuration Management Settings**. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png - :alt: organization variables - -Global variables -~~~~~~~~~~~~~~~~ - -Variables can also be defined globally using the -`OPENWISP_CONTROLLER_CONTEXT <#openwisp-controller-context>`_ setting. - -Template default values -~~~~~~~~~~~~~~~~~~~~~~~ - -It's possible to specify the default values of variables defined in a template. - -This allows to achieve 2 goals: - -1. pass schema validation without errors (otherwise it would not be possible - to save the template in the first place) -2. provide good default values that are valid in most cases but can be - overridden in the device if needed - -These default values will be overridden by the -`User defined device variables <#user-defined-device-variables>`_. - -The default values of variables can be manipulated from the section -"configuration variables" in the edit template page: - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png - :alt: default values - -System defined variables -~~~~~~~~~~~~~~~~~~~~~~~~ - -Predefined device variables, global variables and other variables that -are automatically managed by the system (eg: when using templates of -type VPN-client) are displayed in the admin UI as *System Defined Variables* -in read-only mode. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png - :alt: system defined variables - -**Note:** `Group configuration variables <#group-configuration-variables>`__ -are also added to the **System Defined Variables** of the device. - -Example usage of variables -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Here's a typical use case, the WiFi SSID and WiFi password. -You don't want to define this for every device, but you may want to -allow operators to easily change the SSID or WiFi password for a -specific device without having to re-define the whole wifi interface -to avoid duplicating information. - -This would be the template: - -.. code-block:: json - - { - "interfaces": [ - { - "type": "wireless", - "name": "wlan0", - "wireless": { - "mode": "access_point", - "radio": "radio0", - "ssid": "{{wlan0_ssid}}", - "encryption": { - "protocol": "wpa2_personal", - "key": "{{wlan0_password}}", - "cipher": "auto" - } - } - } - ] - } - -These would be the default values in the template: - -.. code-block:: json - - { - "wlan0_ssid": "SnakeOil PublicWiFi", - "wlan0_password": "Snakeoil_pwd!321654" - } - -The default values can then be overridden at -`device level <#user-defined-device-variables>`_ if needed, eg: - -.. code-block:: json - - { - "wlan0_ssid": "Room 23 ACME Hotel", - "wlan0_password": "room_23pwd!321654" - } - -How to configure push updates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Follow the procedure described below to enable secure SSH access from OpenWISP to your -devices, this is required to enable push updates (whenever the configuration is changed, -OpenWISP will trigger the update in the background) and/or -`firmware upgrades (via the additional module openwisp-firmware-upgrader) -`_. - -**Note**: If you have installed OpenWISP with `openwisp2 Ansbile role `_ -then you can skip the following steps. The Ansible role automatically creates a -default template to update ``authorized_keys`` on networking devices using the -default access credentials. - -1. Generate SSH key -################### - -First of all, we need to generate the SSH key which will be -used by OpenWISP to access the devices, to do so, you can use the following command: - -.. code-block:: shell - - echo './sshkey' | ssh-keygen -t ed25519 -C "openwisp" - -This will create two files in the current directory, one called ``sshkey`` (the private key) and one called -``sshkey.pub`` (the public key). - -Store the content of these files in a secure location. - -**Note:** Support for **ED25519** was added in OpenWrt 21.02 (requires Dropbear > 2020.79). -If you are managing devices with OpenWrt < 21, then you will need to use RSA keys: - -.. code-block:: shell - - echo './sshkey' | ssh-keygen -t rsa -b 4096 -C "openwisp" - -2. Save SSH private key in OpenWISP (access credentials) -######################################################## - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png - :alt: add SSH private key as access credential in OpenWISP - -From the first page of OpenWISP click on "Access credentials", then click -on the **"ADD ACCESS CREDENTIALS"** button in the upper right corner -(alternatively, go to the following URL: ``/admin/connection/credentials/add/``). - -Select SSH as ``type``, enable the **Auto add** checkbox, then at the field -"Credentials type" select "SSH (private key)", now type "root" in the ``username`` field, -while in the ``key`` field you have to paste the contents of the private key just created. - -Now hit save. - -The credentials just created will be automatically enabled for all the devices in the system -(both existing devices and devices which will be added in the future). - -3. Add the public key to your devices -##################################### - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png - :alt: Add authorized SSH public keys template to OpenWISP (OpenWRT) - -Now we need to instruct your devices to allow OpenWISP accessing via SSH, -in order to do this we need to add the contents of the public key file created in step 1 -(``sshkey.pub``) in the file ``/etc/dropbear/authorized_keys`` on the devices, the -recommended way to do this is to create a configuration template in OpenWISP: -from the first page of OpenWISP, click on "Templates", then and click on the -**"ADD TEMPLATE"** button in the upper right corner (alternatively, go to the following URL: -``/admin/config/template/add/``). - -Check **enabled by default**, then scroll down the configuration section, -click on "Configuration Menu", scroll down, click on "Files" then close the menu -by clicking again on "Configuration Menu". Now type ``/etc/dropbear/authorized_keys`` -in the ``path`` field of the file, then paste the contents of ``sshkey.pub`` in ``contents``. - -Now hit save. - -**There's a catch**: you will need to assign the template to any existing device. - -4. Test it -########## - -Once you have performed the 3 steps above, you can test it as follows: - -1. Ensure there's at least one device turned on and connected to OpenWISP, ensure - this device has the "SSH Authorized Keys" assigned to it. -2. Ensure the celery worker of OpenWISP Controller is running (eg: ``ps aux | grep celery``) -3. SSH into the device and wait (maximum 2 minutes) until ``/etc/dropbear/authorized_keys`` - appears as specified in the template. -4. While connected via SSH to the device run the following command in the console: - ``logread -f``, now try changing the device name in OpenWISP -5. Shortly after you change the name in OpenWISP, you should see some output in the - SSH console indicating another SSH access and the configuration update being performed. - -Sending Commands to Devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, there are three options in the **Send Command** dropdown: - -1. Reboot -2. Change Password -3. Custom Command - -While the first two options are self-explanatory, the **custom command** option -allows you to execute any command on the device as shown in the example below. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/commands_demo.gif - :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/commands_demo.gif - :alt: Executing commands on device example - -**Note**: in order for this feature to work, a device needs to have at least -one **Access Credential** (see `How to configure push updates <#how-to-configure-push-updates>`__). - -The **Send Command** button will be hidden until the device -has at least one **Access Credential**. - -If you need to allow your users to quickly send specific commands that are used often in your -network regardless of your users' knowledge of Linux shell commands, you can add new commands -by following instructions in the `"How to define new options in the commands menu" -<#how-to-define-new-options-in-the-commands-menu>`_ section below. - -If you are an advanced user and want to register commands programatically, then refer to -`"Register / Unregistering commands" <#registering--unregistering-commands>`_ section. - -How to define new options in the commands menu -############################################## - -Let's explore to define new custom commands -to help users perform additional management actions -without having to be Linux/Unix experts. - -We can do so by using the ``OPENWISP_CONTROLLER_USER_COMMANDS`` django setting. - -The following example defines a simple command that can ``ping`` an input -``destination_address`` through a network interface, ``interface_name``. - -.. code-block:: python - - # In yourproject/settings.py - - def ping_command_callable(destination_address, interface_name=None): - command = f'ping -c 4 {destination_address}' - if interface_name: - command += f' -I {interface_name}' - return command - - OPENWISP_CONTROLLER_USER_COMMANDS = [ - ( - 'ping', - { - 'label': 'Ping', - 'schema': { - 'title': 'Ping', - 'type': 'object', - 'required': ['destination_address'], - 'properties': { - 'destination_address': { - 'type': 'string', - 'title': 'Destination Address', - }, - 'interface_name': { - 'type': 'string', - 'title': 'Interface Name', - }, - }, - 'message': 'Destination Address cannot be empty', - 'additionalProperties': False, - }, - 'callable': ping_command_callable, - } - ) - ] - -The above code will add the "Ping" command in the user interface as show -in the GIF below: - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/ping_command_example.gif - :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/ping_command_example.gif - :alt: Adding a "ping" command - -The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of ``tuple`` -each containing two elements. The first element of the tuple should contain an -identifier for the command and the second element should contain a ``dict`` -defining configuration of the command. - -Command Configuration -^^^^^^^^^^^^^^^^^^^^^ - -The ``dict`` defining configuration for command should contain following keys: - -1. ``label`` -"""""""""""" - -A ``str`` defining label for the command used internally by Django. - -2. ``schema`` -""""""""""""" - -A ``dict`` defining `JSONSchema `_ for inputs of command. -You can specify the inputs for your command, add rules for performing validation -and make inputs required or optional. - -Here is a detailed explanation of the schema used in above example: - -.. code-block:: python - - { - # Name of the command displayed in "Send Command" widget - 'title': 'Ping', - # Use type "object" if the command needs to accept inputs - # Use type "null" if the command does not accepts any input - 'type': 'object', - # Specify list of inputs that are required - 'required': ['destination_address'], - # Define the inputs for the commands along with their properties - 'properties': { - 'destination_address': { - # type of the input value - 'type': 'string', - # label used for displaying this input field - 'title': 'Destination Address', - }, - 'interface_name': { - 'type': 'string', - 'title': 'Interface Name', - }, - }, - # Error message to be shown if validation fails - 'message': 'Destination Address cannot be empty'), - # Whether specifying addtionaly inputs is allowed from the input form - 'additionalProperties': False, - } - -This example uses only handful of properties available in JSONSchema. You can -experiment with other properties of JSONSchema for schema of your command. - -3. ``callable`` -""""""""""""""" - -A ``callable`` or ``str`` defining dotted path to a callable. It should return -the command (``str``) to be executed on the device. Inputs of the command are -passed as arguments to this callable. - -The example above includes a callable(``ping_command_callable``) for -``ping`` command. - -Registering / Unregistering Commands -#################################### - -OpenWISP Controller provides registering and unregistering commands -through utility functions ``openwisp_controller.connection.commands.register_command`` -and ``openwisp_notifications.types.unregister_notification_type``. -You can use these functions to register or unregister commands -from your code. - -**Note**: These functions are to be used as an alternative to the -`"OPENWISP_CONTROLLER_USER_COMMANDS" <#openwisp-controller-user-commands>`_ -when `developing custom modules based on openwisp-controller -<#extending-openwisp-controller>`_ or when developing custom third party -apps. - -``register_command`` -^^^^^^^^^^^^^^^^^^^^ - -+--------------------+------------------------------------------------------------------+ -| Parameter | Description | -+--------------------+------------------------------------------------------------------+ -| ``command_name`` | A ``str`` defining identifier for the command. | -+--------------------+------------------------------------------------------------------+ -| ``command_config`` | A ``dict`` defining configuration of the command | -| | as shown in `"Command Configuration" <#command-configuration>`_. | -+--------------------+------------------------------------------------------------------+ - -**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already -registered with the same name. - -``unregister_command`` -^^^^^^^^^^^^^^^^^^^^^^ - -+--------------------+-----------------------------------------+ -| Parameter | Description | -+--------------------+-----------------------------------------+ -| ``command_name`` | A ``str`` defining name of the command. | -+--------------------+-----------------------------------------+ - -**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not exists. - -Device Groups -~~~~~~~~~~~~~ - -Device Groups provide features aimed at adding specific management rules -for the devices of an organization: - -- Group similar devices by having dedicated groups for access points, routers, etc. -- Define `group metadata <#group-metadata>`_. -- Define `group configuration templates <#group-templates>`_. -- Define `group configuration variables <#group-configuration-variables>`__. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png - :alt: Device Group example - -Group Templates -############### - -Groups allow to define templates which are automatically assigned to devices -belonging to the group. When using this feature, keep in mind the following -important points: - -- Templates of any configuration backend can be selected, - when a device is assigned to a group, - only the templates which matches the device configuration backend are - applied to the device. -- The system will not force group templates onto devices, this means that - users can remove the applied group templates from a specific device if - needed. -- If a device group is changed, the system will automatically remove the - group templates of the old group and apply the new templates of the new - group (this operation is implemented by leveraging the - `group_templates_changed <#group_templates_changed>`_ signal). -- If the group templates are changed, the devices which belong to the group - will be automatically updated to reflect the changes - (this operation is executed in a background task). -- In case the configuration backend of a device is changed, - the system will handle this automatically too and update the group - templates accordingly (this operation is implemented by leveraging the - `config_backend_changed <#config_backend_changed>`_ signal). -- If a device does not have a configuration defined yet, but it is assigned - to a group which has templates defined, the system will automatically - create a configuration for it using the default backend specified in - `OPENWISP_CONTROLLER_DEFAULT_BACKEND <#OPENWISP_CONTROLLER_DEFAULT_BACKEND>`_ setting. - -**Note:** the list of templates shown in the edit group page do not -contain templates flagged as "default" or "required" to avoid redundancy -because those templates are automatically assigned by the system -to new devices. - -This feature works also when editing group templates or the group assigned -to a device via the `REST API <#change-device-group-detail>`__. - -Group Configuration Variables -############################# - -Groups allow to define configuration variables which are automatically -added to the device's context in the **System Defined Variables**. -Check the `"How to use configuration variables" section <#how-to-use-configuration-variables>`_ -to learn about precedence of different configuration variables. - -This feature works also when editing group templates or the group assigned -to a device via the `REST API <#change-device-group-detail>`__. - -Group Metadata -############## - -Groups allow to store additional information regarding a group in the -structured metadata field (which can be accessed via the REST API). - -The metadata field allows custom structure and validation to standardize -information across all groups using the -`"OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA" <#openwisp-controller-device-group-schema>`_ -setting. - -**Note:** *Group configuration variables* and *Group metadata* serves different purposes. -The group configuration variables should be used when the device configuration is required -to be changed for particular group of devices. Group metadata should be used to store -additional data for the devices. Group metadata is not used for configuration generation. - -Export/Import Device data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png - :alt: Import / Export - -The device list page offers two buttons to export and import device data in -different formats. - -The export feature respects any filters selected in the device list. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :alt: Export - -For importing devices into the system, only the required fields are needed, -for example, the following CSV file will import a device named -``TestImport`` with mac address ``00:11:22:09:44:55`` in the organization with -UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: - - organization,name,mac_address - 3cb5e18c-0312-48ab-8dbd-038b8415bd6f,TestImport,00:11:22:09:44:55 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png - :alt: Import / Export - -Organization Limits -~~~~~~~~~~~~~~~~~~~ - -Allows configuring following limits for each organization: - -- Limit number of devices managed by the organization. - -You can change the limits from the organization's admin page: - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png - :alt: Organization limits - -How to setup WireGuard tunnels -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Follow the procedure described below to setup WireGuard tunnels on your devices. - -**Note:** This example uses **Shared systemwide (no organization)** option as -the organization for VPN server and VPN client template. You can use any -organization as long as VPN server, VPN client template and Device has same -organization. - -1. Create VPN server configuration for WireGuard -################################################ - -1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. We will set **Name** of this VPN server ``Wireguard`` and **Host** as - ``wireguard-server.mydomain.com`` (update this to point to your - WireGuard VPN server). -3. Select ``WireGuard`` from the dropdown as **VPN Backend**. -4. When using WireGuard, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN peer). You can create a new subnet or - select an existing one from the dropdown menu. You can also assign an - **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to - configure. This IP address will be used by the WireGuard interface on - server. -5. We have set the **Webhook Endpoint** as ``https://wireguard-server.mydomain.com:8081/trigger-update`` - for this example. You will need to update this according to you VPN upgrader - endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be - used to ensure that configuration upgrades are requested from trusted - sources. - - **Note**: If you are following this tutorial for also setting up WireGuard - VPN server, just substitute ``wireguard-server.mydomain.com`` with hostname - of your VPN server and follow the steps in next section. - -6. Under the configuration section, set the name of WireGuard tunnel 1 interface. - We have used ``wg0`` in this example. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png - :alt: WireGuard VPN server configuration example 1 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png - :alt: WireGuard VPN server configuration example 2 - -7. After clicking on **Save and continue editing**, you will see that OpenWISP - has automatically created public and private key for WireGuard server in - **System Defined Variables** along with internal IP address information. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png - :alt: WireGuard VPN server configuration example 3 - -2. Deploy Wireguard VPN Server -############################## - -If you haven't already setup WireGuard on your VPN server, this will be a good -time do so. We recommend using the `ansible-wireguard-openwisp `_ -role for installing WireGuard since it also installs scripts that allows -OpenWISP to manage WireGuard VPN server. - -Pay attention to the VPN server attributes used in your playbook. It should be same as -VPN server configuration in OpenWISP. - -3. Create VPN client template for WireGuard VPN Server -###################################################### - -1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``Wireguard Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``Wireguard``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make - OpenWISP to automatically generate public and private keys and provision IP - address for each WireGuard VPN client. -6. After clicking on **Save and continue editing** button, you will see details - of *Wireguard* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration - for this example. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png - :alt: WireGuard VPN client template example - -4. Apply Wireguard VPN template to devices -########################################## - -**Note**: This step assumes that you already have a device registered on -OpenWISP. Register or create a device before proceeding. - -1. Open the **Configuration** tab of the concerned device. -2. Select the *WireGuard Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some - entries in **System Defined Variables**. It will contain internal IP address, - private and public key for the WireGuard client on the device along with - details of WireGuard VPN server. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png - :alt: WireGuard VPN device configuration example - -**Voila!** You have successfully configured OpenWISP to manage WireGuard -tunnels for your devices. - -How to setup VXLAN over WireGuard tunnels -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By following these steps, you will be able to setup layer 2 VXLAN tunnels -encapsulated in WireGuard tunnels which work on layer 3. - -**Note:** This example uses **Shared systemwide (no organization)** option as -the organization for VPN server and VPN client template. You can use any -organization as long as VPN server, VPN client template and Device has same -organization. - -1. Create VPN server configuration for VXLAN over WireGuard -########################################################### - -1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. We will set **Name** of this VPN server ``Wireguard VXLAN`` and **Host** as - ``wireguard-vxlan-server.mydomain.com`` (update this to point to your - WireGuard VXLAN VPN server). -3. Select ``VXLAN over WireGuard`` from the dropdown as **VPN Backend**. -4. When using VXLAN over WireGuard, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN peer). You can create a new subnet or - select an existing one from the dropdown menu. You can also assign an - **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to - configure. This IP address will be used by the WireGuard interface on - server. -5. We have set the **Webhook Endpoint** as ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` - for this example. You will need to update this according to you VPN upgrader - endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be - used to ensure that configuration upgrades are requested from trusted - sources. - - **Note**: If you are following this tutorial for also setting up WireGuard - VPN server, just substitute ``wireguard-server.mydomain.com`` with hostname - of your VPN server and follow the steps in next section. - -6. Under the configuration section, set the name of WireGuard tunnel 1 interface. - We have used ``wg0`` in this example. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png - :alt: WireGuard VPN VXLAN server configuration example 1 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png - :alt: WireGuard VPN VXLAN server configuration example 2 - -7. After clicking on **Save and continue editing**, you will see that OpenWISP - has automatically created public and private key for WireGuard server in - **System Defined Variables** along with internal IP address information. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png - :alt: WireGuard VXLAN VPN server configuration example 3 - -2. Deploy Wireguard VXLAN VPN Server -#################################### - -If you haven't already setup WireGuard on your VPN server, this will be a good -time do so. We recommend using the `ansible-wireguard-openwisp `_ -role for installing WireGuard since it also installs scripts that allows -OpenWISP to manage WireGuard VPN server along with VXLAN tunnels. - -Pay attention to the VPN server attributes used in your playbook. It should be same as -VPN server configuration in OpenWISP. - -3. Create VPN client template for WireGuard VXLAN VPN Server -############################################################ - -1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``Wireguard VXLAN``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make - OpenWISP to automatically generate public and private keys and provision IP - address for each WireGuard VPN client along with VXLAN Network Indentifier(VNI). -6. After clicking on **Save and continue editing** button, you will see details - of *Wireguard VXLAN* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration - for this example. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png - :alt: WireGuard VXLAN VPN client template example - -4. Apply Wireguard VXLAN VPN template to devices -################################################ - -**Note**: This step assumes that you already have a device registered on -OpenWISP. Register or create a device before proceeding. - -1. Open the **Configuration** tab of the concerned device. -2. Select the *WireGuard VXLAN Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some - entries in **System Defined Variables**. It will contain internal IP address, - private and public key for the WireGuard client on the device and details of - WireGuard VPN server along with VXLAN Network Identifier(VNI) of this device. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png - :alt: WireGuard VXLAN VPN device configuration example - -**Voila!** You have successfully configured OpenWISP to manage VXLAN over -WireGuard tunnels for your devices. - -How to setup ZeroTier Tunnels -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Follow the procedure described below to setup ZeroTier tunnels on your devices. - -**Note:** This example uses **Shared systemwide (no organization)** option as -the organization for VPN server and VPN client template. You can use any -organization as long as VPN server, VPN client template and Device has same -organization. - -1. Configure Self-Hosted ZeroTier Network Controller -#################################################### - -If you haven't already set up a self-hosted Zerotier network controller on your server, -now is a good time to do so. You can start by simply installing Zerotier on your server -from the `official website `_. - -2. Create VPN server configuration for ZeroTier -############################################### - -1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. We will set **Name** of this VPN server ``ZeroTier`` and **Host** as - ``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN server). -3. Select ``ZeroTier`` from the dropdown as **VPN Backend**. -4. When using ZeroTier, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN clients (Zerotier network members). - You can create a new subnet or select an existing one from the dropdown menu. - You can also assign an **Internal IP** to the Zerotier controller or - leave it empty for OpenWISP to configure. This IP address will be used - to assign it to the Zerotier controller running on the server. -5. Set the **Webhook AuthToken**, this will be ZeroTier authorization token which you - can obtain by running the following command on the ZeroTier controller: - - .. code-block:: shell - - sudo cat /var/lib/zerotier-one/authtoken.secret - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png - :alt: ZeroTier VPN server configuration example 1 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png - :alt: ZeroTier VPN server configuration example 2 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png - :alt: ZeroTier VPN server configuration example 3 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png - :alt: ZeroTier VPN server configuration example 4 - -6. After clicking on **Save and continue editing**, OpenWISP automatically detects - the node address of the Zerotier controller and creates a Zerotier network. - The **network_id** of this network can be viewed in the **System Defined Variables** - section, where it also provides internal IP address information. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png - :alt: ZeroTier VPN server configuration example 5 - -3. Create VPN client template for ZeroTier VPN Server -##################################################### - -1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``ZeroTier``. -5. Ensure that the **Automatic tunnel provisioning** option is checked. - This will enable OpenWISP to automatically provision an IP address and - ZeroTier identity secrets (used for assigning member IDs) for each ZeroTier VPN client. -6. After clicking on **Save and continue editing** button, you will see details - of *ZeroTier* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration - for this example. - -**Note:** OpenWISP uses `zerotier-idtool -`_ -to manage **ZeroTier identity secrets**. Please make sure that you have -`ZeroTier package installed `_ on the server. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png - :alt: ZeroTier VPN client template example - -4. Apply ZeroTier VPN template to devices -######################################### - -**Note**: This step assumes that you already have a device registered on -OpenWISP. Register or create a device before proceeding. - -1. Open the **Configuration** tab of the concerned device. -2. Select the *ZeroTier Client* template. -3. Upon clicking the **Save and Continue Editing** button, you will see entries - in the **System Defined Variables** section. These entries will include **zerotier_member_id**, **identity_secret**, - and the internal **IP address** of the ZeroTier client (network member) on the device, along with details of the VPN server. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png - :alt: ZeroTier VPN device configuration example 1 - -4. Once the configuration is successfully applied to the device, you will notice a new ZeroTier interface - that is up and running. This interface will have the name ``owzt89f498`` (where ``owzt`` is followed - by the last six hexadecimal characters of the ZeroTier **network ID**). - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png - :alt: ZeroTier VPN device configuration example 2 - -**Voila!** You have successfully configured OpenWISP -to manage ZeroTier tunnels for your devices. - -How to configure automatic provisioning of subnets and IPs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following steps will help you configure automatic provisioning of subnets and IPs -for devices. - -1. Create a Subnet and a Subnet Division Rule -############################################# - -Create a master subnet under which automatically generated subnets will be provisioned. - -**Note**: Choose the size of the subnet appropriately considering your use case. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png - :alt: Creating a master subnet example - -On the same page, add a **subnet division rule** that will be used to provision subnets -under the master subnet. - -The type of subnet division rule controls when subnets and IP addresses will be provisioned -for a device. The subnet division rule types currently implemented are described below. - -Device Subnet Division Rule -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This rule type is triggered whenever a device configuration (``config.Config`` model) -is created for the organization specified in the rule. - -Creating a new rule of "Device" type will also provision subnets and -IP addresses for existing devices of the organization automatically. - -**Note**: a device without a configuration will not trigger this rule. - -VPN Subnet Division Rule -^^^^^^^^^^^^^^^^^^^^^^^^ - -This rule is triggered when a VPN client template is assigned to a device, -provided the VPN server to which the VPN client template relates to has -the same subnet for which the subnet division rule is created. - -**Note:** This rule will only work for **WireGuard** and **VXLAN over WireGuard** -VPN servers. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png - :alt: Creating a subnet division rule example - -In this example, **VPN subnet division rule** is used. - -2. Create a VPN Server -###################### - -Now create a VPN Server and choose the previously created **master subnet** as the subnet for -this VPN Server. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png - :alt: Creating a VPN Server example - -3. Create a VPN Client Template -############################### - -Create a template, setting the **Type** field to **VPN Client** and **VPN** field to use the -previously created VPN Server. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png - :alt: Creating a VPN Client template example - -**Note**: You can also check the **Enable by default** field if you want to automatically -apply this template to devices that will register in future. - -4. Apply VPN Client Template to Devices -####################################### - -With everything in place, you can now apply the VPN Client Template to devices. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png - :alt: Adding template to device example - -After saving the device, you should see all provisioned Subnets and IPs for this device -under `System Defined Variables <#system-defined-variables>`_. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png - :alt: Provisioned Subnets and IPs available as System Defined Variables example - -Voila! You can now use these variables in configuration of the device. Refer to `How to use configuration variables <#how-to-use-configuration-variables>`_ -section of this documentation to learn how to use configuration variables. - -Important notes for using Subnet Division -######################################### - -- In the above example Subnet, VPN Server, and VPN Client Template belonged to the **default** organization. - You can use **Systemwide Shared** Subnet, VPN Server, or VPN Client Template too, but - Subnet Division Rule will be always related to an organization. The Subnet Division Rule will only be - triggered when such VPN Client Template will be applied to a Device having the same organization as Subnet Division Rule. - -- You can also use the configuration variables for provisioned subnets and IPs in the Template. - Each variable will be resolved differently for different devices. E.g. ``OW_subnet1_ip1`` will resolve to - ``10.0.0.1`` for one device and ``10.0.0.55`` for another. Every device gets its own set of subnets and IPs. - But don't forget to provide the default fall back values in the "default values" template field - (used mainly for validation). - -- The Subnet Division Rule will automatically create a reserved subnet, this subnet can be used - to provision any IP addresses that have to be created manually. The rest of the master subnet - address space **must not** be interfered with or the automation implemented in this module - will not work. - -- The above example used `VPN subnet division rule <#vpn-subnet-division-rule>`_. Similarly, - `device subnet division rule <#device-subnet-division-rule>`_ can be used, which only requires - `creating a subnet and a subnet division rule <#1-create-a-subnet-and-a-subnet-division-rule>`_. - -Limitations of Subnet Division -############################## - -In the current implementation, it is not possible to change "Size", "Number of Subnets" and -"Number of IPs" fields of an existing subnet division rule due to following reasons: - -Size -^^^^ - -Allowing to change size of provisioned subnets of an existing subnet division rule -will require rebuilding of Subnets and IP addresses which has possibility of breaking -existing configurations. - -Number of Subnets -^^^^^^^^^^^^^^^^^ - -Allowing to decrease number of subnets of an existing subnet division -rule can create patches of unused subnets dispersed everywhere in the master subnet. -Allowing to increase number of subnets will break the continuous allocation of subnets for -every device. It can also break configuration of devices. - -Number of IPs -^^^^^^^^^^^^^ - -Allowing to decrease number of IPs of an existing subnet division rule -will lead to deletion of IP Addresses which can break configuration of devices being used. -It **is allowed** to increase number of IPs. - -If you want to make changes to any of above fields, delete the existing rule and create a -new one. The automation will provision for all existing devices that meets the criteria -for provisioning. **WARNING**: It is possible that devices get different subnets and IPs -from previous provisioning. - -Default Alerts / Notifications -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+-----------------------+---------------------------------------------------------------------+ -| Notification Type | Use | -+-----------------------+---------------------------------------------------------------------+ -| ``config_error`` | Fires when status of a device configuration changes to ``error``. | -+-----------------------+---------------------------------------------------------------------+ -| ``device_registered`` | Fires when a new device is registered automatically on the network. | -+-----------------------+---------------------------------------------------------------------+ - -REST API Reference ------------------- - -Live documentation -~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.png - -A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. - -Browsable web interface -~~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.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. - -Authentication -~~~~~~~~~~~~~~ - -See openwisp-users: `authenticating with the user token -`_. - -When browsing the API via the `Live documentation <#live-documentation>`_ -or the `Browsable web page <#browsable-web-interface>`_, you can also use -the session authentication by logging in the django admin. - -Pagination -~~~~~~~~~~ - -All *list* endpoints support the ``page_size`` parameter that allows paginating -the results in conjunction with the ``page`` parameter. - -.. code-block:: text - - GET /api/v1/controller/template/?page_size=10 - GET /api/v1/controller/template/?page_size=10&page=2 - -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. - -List devices -############ - -.. code-block:: text - - GET /api/v1/controller/device/ - -**Available filters** - -You can filter a list of devices based on their configuration -status using the ``status`` (e.g modified, applied, or error). - -.. code-block:: text - - GET /api/v1/controller/device/?config__status={status} - -You can filter a list of devices based on their configuration backend -using the ``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). - -.. code-block:: text - - GET /api/v1/controller/device/?config__backend={backend} - -You can filter a list of devices based on their -organization using the ``organization_id`` or ``organization_slug``. - -.. code-block:: text - - GET /api/v1/controller/device/?organization={organization_id} - -.. code-block:: text - - GET /api/v1/controller/device/?organization_slug={organization_slug} - -You can filter a list of devices based on their -configuration templates using the ``template_id``. - -.. code-block:: text - - GET /api/v1/controller/device/?config__templates={template_id} - -You can filter a list of devices based on -their device group using the ``group_id``. - -.. code-block:: text - - GET /api/v1/controller/device/?group={group_id} - -You can filter a list of devices that have a device -location object using the ``with_geo`` (eg. true or false). - -.. code-block:: text - - GET /api/v1/controller/device/?with_geo={with_geo} - -You can filter a list of devices based on -their creation time using the ``creation_time``. - -.. code-block:: text - - # Created exact - GET /api/v1/controller/device/?created={creation_time} - - # Created greater than or equal to - GET /api/v1/controller/device/?created__gte={creation_time} - - # Created is less than - GET /api/v1/controller/device/?created__lt={creation_time} - - - -Create device -############# - -.. code-block:: text - - POST /api/v1/controller/device/ - -Get device detail -################# - -.. code-block:: text - - GET /api/v1/controller/device/{id}/ - -Download device configuration -############################# - -.. code-block:: text - - GET /api/v1/controller/device/{id}/configuration/ - -The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific device. - -Change details of device -######################## - -.. code-block:: text - - PUT /api/v1/controller/device/{id}/ - -Patch details of device -####################### - -.. code-block:: text - - PATCH /api/v1/controller/device/{id}/ - -**Note**: To assign, unassign, and change the order of the assigned templates add, -remove, and change the order of the ``{id}`` of the templates under the ``config`` field in the JSON response respectively. -Moreover, you can also select and unselect templates in the HTML Form of the Browsable API. - -The required template(s) from the organization(s) of the device will added automatically -to the ``config`` and cannot be removed. - -**Example usage**: For assigning template(s) add the/their {id} to the config of a device, - -.. code-block:: shell - - curl -X PATCH \ - http://127.0.0.1:8000/api/v1/controller/device/76b7d9cc-4ffd-4a43-b1b0-8f8befd1a7c0/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: application/json' \ - -d '{ - "config": { - "templates": ["4791fa4c-2cef-4f42-8bb4-c86018d71bd3"] - } - }' - -**Example usage**: For removing assigned templates, simply remove the/their {id} from the config of a device, - -.. code-block:: shell - - curl -X PATCH \ - http://127.0.0.1:8000/api/v1/controller/device/76b7d9cc-4ffd-4a43-b1b0-8f8befd1a7c0/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: application/json' \ - -d '{ - "config": { - "templates": [] - } - }' - -**Example usage**: For reordering the templates simply change their order from the config of a device, - -.. code-block:: shell - - curl -X PATCH \ - http://127.0.0.1:8000/api/v1/controller/device/76b7d9cc-4ffd-4a43-b1b0-8f8befd1a7c0/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'cache-control: no-cache' \ - -H 'content-type: application/json' \ - -H 'postman-token: b3f6a1cc-ff13-5eba-e460-8f394e485801' \ - -d '{ - "config": { - "templates": [ - "c5bbc697-170e-44bc-8eb7-b944b55ee88f", - "4791fa4c-2cef-4f42-8bb4-c86018d71bd3" - ] - } - }' - -Delete device -############# - -.. code-block:: text - - DELETE /api/v1/controller/device/{id}/ - -List device connections -####################### - -.. code-block:: text - - GET /api/v1/controller/device/{id}/connection/ - -Create device connection -######################## - -.. code-block:: text - - POST /api/v1/controller/device/{id}/connection/ - -Get device connection detail -############################ - -.. code-block:: text - - GET /api/v1/controller/device/{id}/connection/{id}/ - -Change device connection detail -############################### - -.. code-block:: text - - PUT /api/v1/controller/device/{id}/connection/{id}/ - -Patch device connection detail -############################## - -.. code-block:: text - - PATCH /api/v1/controller/device/{id}/connection/{id}/ - -Delete device connection -######################## - -.. code-block:: text - - DELETE /api/v1/controller/device/{id}/connection/{id}/ - -List credentials -################ - -.. code-block:: text - - GET /api/v1/connection/credential/ - -Create credential -################# - -.. code-block:: text - - POST /api/v1/connection/credential/ - -Get credential detail -##################### - -.. code-block:: text - - GET /api/v1/connection/credential/{id}/ - -Change credential detail -######################## - -.. code-block:: text - - PUT /api/v1/connection/credential/{id}/ - -Patch credential detail -####################### - -.. code-block:: text - - PATCH /api/v1/connection/credential/{id}/ - -Delete credential -################# - -.. code-block:: text - - DELETE /api/v1/connection/credential/{id}/ - -List commands of a device -######################### - -.. code-block:: text - - GET /api/v1/controller/device/{id}/command/ - -Execute a command a device -########################## - -.. code-block:: text - - POST /api/v1/controller/device/{id}/command/ - -Get command details -################### - -.. code-block:: text - - GET /api/v1/controller/device/{device_id}/command/{command_id}/ - -List device groups -################## - -.. code-block:: text - - GET /api/v1/controller/group/ - -**Available filters** - -You can filter a list of device groups based on their -organization using the ``organization_id`` or ``organization_slug``. - -.. code-block:: text - - GET /api/v1/controller/group/?organization={organization_id} - -.. code-block:: text - - GET /api/v1/controller/group/?organization_slug={organization_slug} - -You can filter a list of device groups that have a -device object using the ``empty`` (eg. true or false). - -.. code-block:: text - - GET /api/v1/controller/group/?empty={empty} - - -Create device group -################### - -.. code-block:: text - - POST /api/v1/controller/group/ - -Get device group detail -####################### - -.. code-block:: text - - GET /api/v1/controller/group/{id}/ - -Change device group detail -########################## - -.. code-block:: text - - PUT /api/v1/controller/group/{id}/ - -This endpoint allows to change the `group templates <#group-templates>`_ too. - -Get device group from certificate common name -############################################# - -.. code-block:: text - - GET /api/v1/controller/cert/{common_name}/group/ - -This endpoint can be used to retrieve group information and metadata by the -common name of a certificate used in a VPN client tunnel, this endpoint is -used in layer 2 tunneling solutions for firewall/captive portals. - -It is also possible to filter device group by providing organization slug -of certificate's organization as show in the example below: - -.. code-block:: text - - GET /api/v1/controller/cert/{common_name}/group/?org={org1_slug},{org2_slug} - -Get device location -################### - -.. code-block:: text - - - GET /api/v1/controller/device/{id}/location/ - - -Create device location -###################### - -.. code-block:: text - - PUT /api/v1/controller/device/{id}/location/ - -You can create ``DeviceLocation`` object by using primary -keys of existing ``Location`` and ``FloorPlan`` objects as shown in -the example below. - -.. code-block:: json - - { - "location": "f0cb5762-3711-4791-95b6-c2f6656249fa", - "floorplan": "dfeb6724-aab4-4533-aeab-f7feb6648acd", - "indoor": "-36,264" - } - -**Note:** The ``indoor`` field represents the coordinates of the -point placed on the image from the top left corner. E.g. if you -placed the pointer on the top left corner of the floorplan image, -its indoor coordinates will be ``0,0``. - -.. code-block:: text - - curl -X PUT \ - http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: application/json' \ - -d '{ - "location": "f0cb5762-3711-4791-95b6-c2f6656249fa", - "floorplan": "dfeb6724-aab4-4533-aeab-f7feb6648acd", - "indoor": "-36,264" - }' - -You can also create related ``Location`` and ``FloorPlan`` objects for the -device directly from this endpoint. - -The following example demonstrates creating related location -object in a single request. - -.. code-block:: json - - { - "location": { - "name": "Via del Corso", - "address": "Via del Corso, Roma, Italia", - "geometry": { - "type": "Point", - "coordinates": [12.512124, 41.898903] - }, - "type": "outdoor", - } - } - -.. code-block:: text - - curl -X PUT \ - http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: application/json' \ - -d '{ - "location": { - "name": "Via del Corso", - "address": "Via del Corso, Roma, Italia", - "geometry": { - "type": "Point", - "coordinates": [12.512124, 41.898903] - }, - "type": "outdoor" - } - }' - -**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** -format, like following: - -.. code-block:: json - - { - "location": { - "name": "Via del Corso", - "address": "Via del Corso, Roma, Italia", - "geometry": "POINT (12.512124 41.898903)", - "type": "outdoor", - } - } - -Similarly, you can create ``Floorplan`` object with the same request. -But, note that a ``FloorPlan`` can be added to ``DeviceLocation`` only -if the related ``Location`` object defines an indoor location. The example -below demonstrates creating both ``Location`` and ``FloorPlan`` objects. - -.. code-block:: text - - // This is not a valid JSON object. The JSON format is - // only used for showing available fields. - { - "location.name": "Via del Corso", - "location.address": "Via del Corso, Roma, Italia", - "location.geometry.type": "Point", - "location.geometry.coordinates": [12.512124, 41.898903] - "location.type": "outdoor", - "floorplan.floor": 1, - "floorplan.image": floorplan.png, - } - -.. code-block:: text - - curl -X PUT \ - http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ - -F 'location.name=Via del Corso' \ - -F 'location.address=Via del Corso, Roma, Italia' \ - -F location.geometry.type=Point \ - -F 'location.geometry.coordinates=[12.512124, 41.898903]' \ - -F location.type=indoor \ - -F floorplan.floor=1 \ - -F 'floorplan.image=@floorplan.png' - -**Note:** The request in above example uses ``multipart content-type`` -for uploading floorplan image. - -You can also use an existing ``Location`` object and create a new -floorplan for that location using this endpoint. - -.. code-block:: text - - // This is not a valid JSON object. The JSON format is - // only used for showing available fields. - { - "location": "f0cb5762-3711-4791-95b6-c2f6656249fa", - "floorplan.floor": 1, - "floorplan.image": floorplan.png - } - -.. code-block:: text - - curl -X PUT \ - http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ - -F location=f0cb5762-3711-4791-95b6-c2f6656249fa \ - -F floorplan.floor=1 \ - -F 'floorplan.image=@floorplan.png' - -Change details of device location -################################# - -.. code-block:: text - - PUT /api/v1/controller/device/{id}/location/ - -**Note:** This endpoint can be used to update related ``Location`` -and ``Floorplan`` objects. Refer `examples of "Create device location" -section for information on payload format <#create-device-location>`_. - -Delete device location -###################### - -.. code-block:: text - - DELETE /api/v1/controller/device/{id}/location/ - -Get device coordinates -###################### - -.. code-block:: text - - GET /api/v1/controller/device/{id}/coordinates/ - -**Note:** This endpoint is intended to be used by devices. - -This endpoint skips multi-tenancy and permission checks if the -device ``key`` is passed as ``query_param`` because the system -assumes that the device is updating it's position. - -.. code-block:: text - - curl -X GET \ - 'http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/coordinates/?key=10a0cb5a553c71099c0e4ef236435496' - -Update device coordinates -######################### - -.. code-block:: text - - PUT /api/v1/controller/device/{id}/coordinates/ - -**Note:** This endpoint is intended to be used by devices. - -This endpoint skips multi-tenancy and permission checks if the -device ``key`` is passed as ``query_param`` because the system -assumes that the device is updating it's position. - -.. code-block:: json - - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [12.512124, 41.898903] - }, - } - -.. code-block:: text - - curl -X PUT \ - 'http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/coordinates/?key=10a0cb5a553c71099c0e4ef236435496' \ - -H 'content-type: application/json' \ - -d '{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [12.512124, 41.898903] - }, - }' - -List locations -############## - -.. code-block:: text - - GET /api/v1/controller/location/ - -**Available filters** - -You can filter using ``organization_id`` or ``organization_slug`` -to get list locations that belongs to an organization. - -.. code-block:: text - - GET /api/v1/controller/location/?organization={organization_id} - -.. code-block:: text - - GET /api/v1/controller/location/?organization_slug={organization_slug} - -Create location -############### - -.. code-block:: text - - POST /api/v1/controller/location/ - -If you are creating an ``indoor`` location, you can use this endpoint -to create floorplan for the location. - -The following example demonstrates creating floorplan along with location -in a single request. - -.. code-block:: text - - { - "name": "Via del Corso", - "address": "Via del Corso, Roma, Italia", - "geometry.type": "Point", - "geometry.location": [12.512124, 41.898903], - "type": "indoor", - "is_mobile": "false", - "floorplan.floor": "1", - "floorplan.image": floorplan.png, - "organization": "1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05" - } - -.. code-block:: text - - curl -X POST \ - http://127.0.0.1:8000/api/v1/controller/location/ \ - -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ - -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ - -F 'name=Via del Corso' \ - -F 'address=Via del Corso, Roma, Italia' \ - -F geometry.type=Point \ - -F 'geometry.coordinates=[12.512124, 41.898903]' \ - -F type=indoor \ - -F is_mobile=false \ - -F floorplan.floor=1 \ - -F 'floorplan.image=@floorplan.png' \ - -F organization=1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05 - -**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** -format, like following: - -.. code-block:: text - - { - "name": "Via del Corso", - "address": "Via del Corso, Roma, Italia", - "geometry": "POINT (12.512124 41.898903)", - "type": "indoor", - "is_mobile": "false", - "floorplan.floor": "1", - "floorplan.image": floorplan.png, - "organization": "1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05" - } - -Get location details -#################### - -.. code-block:: text - - GET /api/v1/controller/location/{pk}/ - -Change location details -####################### - -.. code-block:: text - - PUT /api/v1/controller/location/{pk}/ - -**Note**: Only the first floorplan data present can be -edited or changed. Setting the ``type`` of location to -outdoor will remove all the floorplans associated with it. - -Refer `examples of "Create location" -section for information on payload format <#create-location>`_. - -Delete location -############### - -.. code-block:: text - - DELETE /api/v1/controller/location/{pk}/ - -List devices in a location -########################## - -.. code-block:: text - - GET /api/v1/controller/location/{id}/device/ - -List locations with devices deployed (in GeoJSON format) -######################################################## - -**Note**: this endpoint will only list locations that have been assigned to a device. - -.. code-block:: text - - GET /api/v1/controller/location/geojson/ - -**Available filters** - -You can filter using ``organization_id`` or ``organization_slug`` -to get list location of devices from that organization. - -.. code-block:: text - - GET /api/v1/controller/location/geojson/?organization_id={organization_id} - -.. code-block:: text - - GET /api/v1/controller/location/geojson/?organization_slug={organization_slug} - -List floorplans -############### - -.. code-block:: text - - GET /api/v1/controller/floorplan/ - -**Available filters** - -You can filter using ``organization_id`` or ``organization_slug`` -to get list floorplans that belongs to an organization. - -.. code-block:: text - - GET /api/v1/controller/floorplan/?organization={organization_id} - -.. code-block:: text - - GET /api/v1/controller/floorplan/?organization_slug={organization_slug} - -Create floorplan -################ - -.. code-block:: text - - POST /api/v1/controller/floorplan/ - -Get floorplan details -##################### - -.. code-block:: text - - GET /api/v1/controller/floorplan/{pk}/ - -Change floorplan details -######################## - -.. code-block:: text - - PUT /api/v1/controller/floorplan/{pk}/ - -Delete floorplan -################ - -.. code-block:: text - - DELETE /api/v1/controller/floorplan/{pk}/ - -List templates -############## - -.. code-block:: text - - GET /api/v1/controller/template/ - -**Available filters** - -You can filter a list of templates based on their organization -using the ``organization_id`` or ``organization_slug``. - -.. code-block:: text - - GET /api/v1/controller/template/?organization={organization_id} - -.. code-block:: text - - GET /api/v1/controller/template/?organization_slug={organization_slug} - -You can filter a list of templates based on their backend using -the ``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). - -.. code-block:: text - - GET /api/v1/controller/template/?backend={backend} - -You can filter a list of templates based on their -type using the ``type`` (eg. vpn or generic). - -.. code-block:: text - - GET /api/v1/controller/template/?type={type} - -You can filter a list of templates that are enabled -by default or not using the ``default`` (eg. true or false). - -.. code-block:: text - - GET /api/v1/controller/template/?default={default} - -You can filter a list of templates that are required -or not using the ``required`` (eg. true or false). - -.. code-block:: text - - GET /api/v1/controller/template/?required={required} - -You can filter a list of templates based on -their creation time using the ``creation_time``. - -.. code-block:: text - - # Created exact - - GET /api/v1/controller/template/?created={creation_time} - - # Created greater than or equal to - - GET /api/v1/controller/template/?created__gte={creation_time} - - # Created is less than - - GET /api/v1/controller/template/?created__lt={creation_time} - -Create template -############### - -.. code-block:: text - - POST /api/v1/controller/template/ - -Get template detail -################### - -.. code-block:: text - - GET /api/v1/controller/template/{id}/ - -Download template configuration -############################### - -.. code-block:: text - - GET /api/v1/controller/template/{id}/configuration/ - -The above endpoint triggers the download of a ``tar.gz`` file -containing the generated configuration for that specific template. - -Change details of template -########################## - -.. code-block:: text - - PUT /api/v1/controller/template/{id}/ - -Patch details of template -######################### - -.. code-block:: text - - PATCH /api/v1/controller/template/{id}/ - -Delete template -############### - -.. code-block:: text - - DELETE /api/v1/controller/template/{id}/ - -List VPNs -######### - -.. code-block:: text - - GET /api/v1/controller/vpn/ - -**Available filters** - -You can filter a list of vpns based -on their backend using the ``backend`` -(e.g openwisp_controller.vpn_backends.OpenVpn -or openwisp_controller.vpn_backends.Wireguard). - -.. code-block:: text - - GET /api/v1/controller/vpn/?backend={backend} - -You can filter a list of vpns based on their subnet using the ``subnet_id``. - -.. code-block:: text - - GET /api/v1/controller/vpn/?subnet={subnet_id} - -You can filter a list of vpns based on their organization -using the ``organization_id`` or ``organization_slug``. - -.. code-block:: text - - GET /api/v1/controller/vpn/?organization={organization_id} - -.. code-block:: text - - GET /api/v1/controller/vpn/?organization_slug={organization_slug} - -Create VPN -########## - -.. code-block:: text - - POST /api/v1/controller/vpn/ - -Get VPN detail -############## - -.. code-block:: text - - GET /api/v1/controller/vpn/{id}/ - -Download VPN configuration -########################## - -.. code-block:: text - - GET /api/v1/controller/vpn/{id}/configuration/ - -The above endpoint triggers the download of a ``tar.gz`` file -containing the generated configuration for that specific VPN. - -Change details of VPN -##################### - -.. code-block:: text - - PUT /api/v1/controller/vpn/{id}/ - -Patch details of VPN -#################### - -.. code-block:: text - - PATCH /api/v1/controller/vpn/{id}/ - -Delete VPN -########## - -.. code-block:: text - - DELETE /api/v1/controller/vpn/{id}/ - -List CA -####### - -.. code-block:: text - - GET /api/v1/controller/ca/ - -Create new CA -############# - -.. code-block:: text - - POST /api/v1/controller/ca/ - -Import existing CA -################## - -.. code-block:: text - - POST /api/v1/controller/ca/ - -**Note**: To import an existing CA, only ``name``, ``certificate`` -and ``private_key`` fields have to be filled in the ``HTML`` form or -included in the ``JSON`` format. - -Get CA Detail -############# - -.. code-block:: text - - GET /api/v1/controller/ca/{id}/ - -Change details of CA -#################### - -.. code-block:: text - - PUT /api/v1/controller/ca/{id}/ - -Patch details of CA -################### - -.. code-block:: text - - PATCH /api/v1/controller/ca/{id}/ - -Download CA(crl) -################ - -.. code-block:: text - - GET /api/v1/controller/ca/{id}/crl/ - -The above endpoint triggers the download of ``{id}.crl`` file containing -up to date CRL of that specific CA. - -Delete CA -######### - -.. code-block:: text - - DELETE /api/v1/controller/ca/{id}/ - -Renew CA -######## - -.. code-block:: text - - POST /api/v1/controller/ca/{id}/renew/ - -List Cert -######### - -.. code-block:: text - - GET /api/v1/controller/cert/ - -Create new Cert -############### - -.. code-block:: text - - POST /api/v1/controller/cert/ - -Import existing Cert -#################### - -.. code-block:: text - - POST /api/v1/controller/cert/ - -**Note**: To import an existing Cert, only ``name``, ``ca``, -``certificate`` and ``private_key`` fields have to be filled -in the ``HTML`` form or included in the ``JSON`` format. - -Get Cert Detail -############### - -.. code-block:: text - - GET /api/v1/controller/cert/{id}/ - -Change details of Cert -###################### - -.. code-block:: text - - PUT /api/v1/controller/cert/{id}/ - -Patch details of Cert -##################### - -.. code-block:: text - - PATCH /api/v1/controller/cert/{id}/ - -Delete Cert -########### - -.. code-block:: text - - DELETE /api/v1/controller/cert/{id}/ - -Renew Cert -########## - -.. code-block:: text - - POST /api/v1/controller/cert/{id}/renew/ - -Revoke Cert -########### - -.. code-block:: text - - POST /api/v1/controller/cert/{id}/revoke/ - -Settings --------- - -You can change the values for the following variables in -``settings.py`` to configure your instance of openwisp-controller. - -``OPENWISP_SSH_AUTH_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``2`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ - -Configure timeout to wait for an authentication response when establishing a SSH connection. - -``OPENWISP_SSH_BANNER_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``60`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ - -Configure timeout to wait for the banner to be presented when establishing a SSH connection. - -``OPENWISP_SSH_COMMAND_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``30`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ - -Configure timeout on blocking read/write operations when executing a command in a SSH connection. - -``OPENWISP_SSH_CONNECTION_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``5`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ - -Configure timeout for the TCP connect when establishing a SSH connection. - -``OPENWISP_CONNECTORS`` -~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+------------------------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.connection.connectors.ssh.Ssh', 'SSH'), | -| | ('openwisp_controller.connection.connectors.openwrt.snmp.OpenWRTSnmp', 'OpenWRT SNMP'), | -| | ('openwisp_controller.connection.connectors.airos.snmp.AirOsSnmp', 'Ubiquiti AirOS SNMP'), | -| | ) | -+--------------+------------------------------------------------------------------------------------------------+ - -Available connector classes. Connectors are python classes that specify ways -in which OpenWISP can connect to devices in order to launch commands. - -``OPENWISP_UPDATE_STRATEGIES`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+----------------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt', 'OpenWRT SSH'), | -| | ) | -+--------------+----------------------------------------------------------------------------------------+ - -Available update strategies. An update strategy is a subclass of a -connector class which defines an ``update_config`` method which is -in charge of updating the configuration of the device. - -This operation is launched in a background worker when the configuration -of a device is changed. - -It's possible to write custom update strategies and add them to this -setting to make them available in OpenWISP. - -``OPENWISP_CONFIG_UPDATE_MAPPING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | { | -| | 'netjsonconfig.OpenWrt': OPENWISP_UPDATE_STRATEGIES[0][0], | -| | } | -+--------------+--------------------------------------------------------------------+ - -A dictionary that maps configuration backends to update strategies in order to -automatically determine the update strategy of a device connection if the -update strategy field is left blank by the user. - -``OPENWISP_CONTROLLER_BACKENDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+-----------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('netjsonconfig.OpenWrt', 'OpenWRT'), | -| | ('netjsonconfig.OpenWisp', 'OpenWISP'), | -| | ) | -+--------------+-----------------------------------------------+ - -Available configuration backends. For more information, see `netjsonconfig backends -`_. - -``OPENWISP_CONTROLLER_VPN_BACKENDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+----------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.vpn_backends.OpenVpn', 'OpenVPN'), | -| | ('openwisp_controller.vpn_backends.Wireguard', 'WireGuard'), | -| | ('openwisp_controller.vpn_backends.VxlanWireguard', 'VXLAN over WireGuard'), | -| | ('openwisp_controller.vpn_backends.ZeroTier', 'ZeroTier'), | -| | ) | -+--------------+----------------------------------------------------------------------------------+ - -Available VPN backends for VPN Server objects. For more information, see `netjsonconfig VPN backends -`_. - -A VPN backend must follow some basic rules in order to be compatible with *openwisp-controller*: - -* it MUST allow at minimum and at maximum one VPN instance -* the main *NetJSON* property MUST match the lowercase version of the class name, - eg: when using the ``OpenVpn`` backend, the system will look into - ``config['openvpn']`` -* it SHOULD focus on the server capabilities of the VPN software being used - -``OPENWISP_CONTROLLER_DEFAULT_BACKEND`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------------------+ -| **type**: | ``str`` | -+--------------+----------------------------------------+ -| **default**: | ``OPENWISP_CONTROLLER_BACKENDS[0][0]`` | -+--------------+----------------------------------------+ - -The preferred backend that will be used as initial value when adding new ``Config`` or -``Template`` objects in the admin. - -This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_BACKENDS`` setting, -which is ``netjsonconfig.OpenWrt``. - -Setting it to ``None`` will force the user to choose explicitly. - -``OPENWISP_CONTROLLER_DEFAULT_VPN_BACKEND`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------+ -| **type**: | ``str`` | -+--------------+--------------------------------------------+ -| **default**: | ``OPENWISP_CONTROLLER_VPN_BACKENDS[0][0]`` | -+--------------+--------------------------------------------+ - -The preferred backend that will be used as initial value when adding new ``Vpn`` objects in the admin. - -This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_VPN_BACKENDS`` setting, -which is ``openwisp_controller.vpn_backends.OpenVpn``. - -Setting it to ``None`` will force the user to choose explicitly. - -``OPENWISP_CONTROLLER_REGISTRATION_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -Whether devices can automatically register through the controller or not. - -This feature is enabled by default. - -Autoregistration must be supported on the devices in order to work, see `openwisp-config automatic -registration `_ for more information. - -``OPENWISP_CONTROLLER_CONSISTENT_REGISTRATION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -Whether devices that are already registered are recognized when reflashed or reset, hence keeping -the existing configuration without creating a new one. - -This feature is enabled by default. - -Autoregistration must be enabled also on the devices in order to work, see `openwisp-config -consistent key generation `_ -for more information. - -``OPENWISP_CONTROLLER_REGISTRATION_SELF_CREATION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -Whether devices that are not already present in the system are allowed to register or not. - -Turn this off if you still want to use auto-registration to avoid having to -manually set the device UUID and key in its configuration file but also want -to avoid indiscriminate registration of new devices without explicit permission. - -``OPENWISP_CONTROLLER_CONTEXT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------+ -| **type**: | ``dict`` | -+--------------+------------------+ -| **default**: | ``{}`` | -+--------------+------------------+ - -Additional context that is passed to the default context of each device object. - -``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide configuration variables. - -For more information regarding how to use configuration variables in OpenWISP, -see `How to use configuration variables <#how-to-use-configuration-variables>`_. - -For technical information about how variables are handled in the lower levels -of OpenWISP, see `netjsonconfig context: configuration variables -`_. - -``OPENWISP_CONTROLLER_DEFAULT_AUTO_CERT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+---------------------------+ -| **type**: | ``bool`` | -+--------------+---------------------------+ -| **default**: | ``True`` | -+--------------+---------------------------+ - -The default value of the ``auto_cert`` field for new ``Template`` objects. - -The ``auto_cert`` field is valid only for templates which have ``type`` -set to ``VPN`` and indicates whether configuration regarding the VPN tunnel is -provisioned automatically to each device using the template, eg: - -- when using OpenVPN, new `x509 `_ certificates - will be generated automatically using the same CA assigned to the related VPN object -- when using WireGuard, new pair of private and public keys - (using `Curve25519 `_) will be generated, as well as - an IP address of the subnet assigned to the related VPN object -- when using `VXLAN `_ tunnels over Wireguad, - in addition to the configuration generated for WireGuard, a new VID will be generated - automatically for each device if the configuration option "auto VNI" is turned on in - the VPN object - -All these auto generated configuration options will be available as -template variables. - -The objects that are automatically created will also be removed when they are not -needed anymore (eg: when the VPN template is removed from a configuration object). - -``OPENWISP_CONTROLLER_CERT_PATH`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+---------------------------+ -| **type**: | ``str`` | -+--------------+---------------------------+ -| **default**: | ``/etc/x509`` | -+--------------+---------------------------+ - -The filesystem path where x509 certificate will be installed when -downloaded on routers when ``auto_cert`` is being used (enabled by default). - -``OPENWISP_CONTROLLER_COMMON_NAME_FORMAT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------+ -| **type**: | ``str`` | -+--------------+------------------------------+ -| **default**: | ``{mac_address}-{name}`` | -+--------------+------------------------------+ - -Defines the format of the ``common_name`` attribute of VPN client certificates -that are automatically created when using VPN templates which have ``auto_cert`` -set to ``True``. A unique slug generated using `shortuuid `_ -is appended to the common name to introduce uniqueness. Therefore, resulting -common names will have ``{OPENWISP_CONTROLLER_COMMON_NAME_FORMAT}-{unique-slug}`` -format. - -**Note:** If the ``name`` and ``mac address`` of the device are equal, -the ``name`` of the device will be omitted from the common name to avoid redundancy. - -``OPENWISP_CONTROLLER_MANAGEMENT_IP_DEVICE_LIST`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------+ -| **type**: | ``bool`` | -+--------------+------------------------------+ -| **default**: | ``True`` | -+--------------+------------------------------+ - -In the device list page, the column ``IP`` will show the ``management_ip`` if -available, defaulting to ``last_ip`` otherwise. - -If this setting is set to ``False`` the ``management_ip`` won't be shown -in the device list page even if present, it will be shown only in the device -detail page. - -You may set this to ``False`` if for some reason the majority of your user -doesn't care about the management ip address. - -``OPENWISP_CONTROLLER_CONFIG_BACKEND_FIELD_SHOWN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------+ -| **type**: | ``bool`` | -+--------------+------------------------------+ -| **default**: | ``True`` | -+--------------+------------------------------+ - -This setting toggles the ``backend`` fields in add/edit pages in Device and Template configuration, -as well as the ``backend`` field/filter in Device list and Template list. - -If this setting is set to ``False`` these items will be removed from the UI. - -Note: This setting affects only the configuration backend and NOT the VPN backend. - -``OPENWISP_CONTROLLER_DEVICE_NAME_UNIQUE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -This setting conditionally enforces unique Device names in an Organization. -The query to enforce this is case-insensitive. - -Note: For this constraint to be optional, it is enforced on an application level and not on database. - -``OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``False`` | -+--------------+-------------+ - -The field ``hardware_id`` can be used to store a unique hardware id, for example a serial number. - -If this setting is set to ``True`` then this field will be shown first in the device list page -and in the add/edit device page. - -This feature is disabled by default. - -``OPENWISP_CONTROLLER_HARDWARE_ID_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | { | -| | 'blank': not OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED, | -| | 'null': True, | -| | 'max_length': 32, | -| | 'unique': True, | -| | 'verbose_name': _('Serial number'), | -| | 'help_text': _('Serial number of this device') | -| | } | -+--------------+--------------------------------------------------------------+ - -Options for the model field ``hardware_id``. - -* ``blank``: wether the field is allowed to be blank -* ``null``: wether an empty value will be stored as ``NULL`` in the database -* ``max_length``: maximum length of the field -* ``unique``: wether the value of the field must be unique -* ``verbose_name``: text for the human readable label of the field -* ``help_text``: help text to be displayed with the field - -``OPENWISP_CONTROLLER_HARDWARE_ID_AS_NAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -When the hardware ID feature is enabled, devices will be referenced with -their hardware ID instead of their name. - -If you still want to reference devices by their name, set this to ``False``. - -``OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------+ -| **type**: | ``tuple`` | -+--------------+----------------------------+ -| **default**: | ``('Device', 'Devices')`` | -+--------------+----------------------------+ - -Defines the ``verbose_name`` attribute of the ``Device`` model, which is displayed in the -admin site. The first and second element of the tuple represent the singular and plural forms. - -For example, if we want to change the verbose name to "Hotspot", we could write: - -.. code-block:: python - - OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME = ('Hotspot', 'Hotspots') - -``OPENWISP_CONTROLLER_HIDE_AUTOMATICALLY_GENERATED_SUBNETS_AND_IPS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``bool`` | -+--------------+-----------+ -| **default**: | ``False`` | -+--------------+-----------+ - -Setting this to ``True`` will hide subnets and IPs generated using `subnet division rules <#subnet-division-app>`_ -from being displayed on the changelist view of Subnet and IP admin. - -``OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+---------------------------------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+---------------------------------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType', 'Device'), | -| | ('openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType', 'VPN'), | -| | ) | -| | | -+--------------+---------------------------------------------------------------------------------------------------------+ - -`Available types for Subject Division Rule <#device-subnet-division-rule>`_ objects. -For more information on how to write your own types, read -`"Custom Subnet Division Rule Types" section of this documentation <#custom-subnet-division-rule-types>`_ - -``OPENWISP_CONTROLLER_API`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``bool`` | -+--------------+-----------+ -| **default**: | ``True`` | -+--------------+-----------+ - -Indicates whether the API for Openwisp Controller is enabled or not. -To disable the API by default add `OPENWISP_CONTROLLER_API = False` in `settings.py` file. - -``OPENWISP_CONTROLLER_API_HOST`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``str`` | -+--------------+-----------+ -| **default**: | ``None`` | -+--------------+-----------+ - -Allows to specify backend URL for API requests, if the frontend is hosted separately. - -``OPENWISP_CONTROLLER_USER_COMMANDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------+ -| **type**: | ``list`` | -+--------------+----------+ -| **default**: | ``[]`` | -+--------------+----------+ - -Allows to specify a ``list`` of tuples for adding commands as described in -`'How to define custom commands" <#how-to-define-new-options-in-the-commands-menu>`_ section. - -``OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | { | -| | # By default all commands are allowed | -| | '__all__': '*', | -| | } | -| | | -+--------------+------------------------------------------------+ - -This setting controls the command types that are enabled on the system -By default, all command types are enabled to all the organizations, -but it's possible to disable a specific command for a specific organization -as shown in the following example: - -.. code-block:: python - - OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS = { - '__all__': '*', - # Organization UUID: # Tuple of enabled commands - '7448a190-6e65-42bf-b8ea-bb6603e593a5': ('reboot', 'change_password'), - } - -In the example above, the organization with UUID ``7448a190-6e65-42bf-b8ea-bb6603e593a5`` -will allow to send only commands of type ``reboot`` and ``change_password``, -while all the other organizations will have all command types enabled. - -``OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------------------+ -| **type**: | ``dict`` | -+--------------+------------------------------------------+ -| **default**: | ``{'type': 'object', 'properties': {}}`` | -+--------------+------------------------------------------+ - -Allows specifying JSONSchema used for validating meta-data of `Device Group <#device-groups>`__. - -``OPENWISP_CONTROLLER_SHARED_MANAGEMENT_IP_ADDRESS_SPACE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------+ -| **type**: | ``bool`` | -+--------------+----------+ -| **default**: | ``True`` | -+--------------+----------+ - -By default, the system assumes that the address space of the management -tunnel is shared among all the organizations using the system, that is, -the system assumes there's only one management VPN, tunnel or other -networking technology to reach the devices it controls. - -When set to ``True``, any device belonging to any -organization will never have the same ``management_ip`` as another device, -the latest device declaring the management IP will take the IP and any -other device who declared the same IP in the past will have the field -reset to empty state to avoid potential conflicts. - -Set this to ``False`` if every organization has its dedicated management -tunnel with a dedicated address space that is reachable by the OpenWISP server. - -``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ - -By default, only the management IP will be used to establish connection -with 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``. - -``OPENWISP_CONTROLLER_DSA_OS_MAPPING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------+ -| **type**: | ``dict`` | -+--------------+----------+ -| **default**: | ``{}`` | -+--------------+----------+ - -OpenWISP Controller can figure out whether it should use the new OpenWrt syntax -for DSA interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by -reading the ``os`` field of the ``Device`` object. However, if the firmware you -are using has a custom firmware identifier, the system will not be able to figure -out whether it should use the new syntax and it will default to -`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK <#openwisp_controller_dsa_default_fallback>`_. - -If you want to make sure the system can parse your custom firmware -identifier properly, you can follow the example below. - -For the sake of the example, the OS identifier ``MyCustomFirmware 2.0`` -corresponds to ``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` corresponds to -``OpenWrt 21.02``. Configuring this setting as indicated below will allow -OpenWISP to supply the right syntax automatically. - -Example: - -.. code-block:: python - - OPENWISP_CONTROLLER_DSA_OS_MAPPING = { - 'netjsonconfig.OpenWrt': { - # OpenWrt >=21.02 configuration syntax will be used for - # these OS identifiers. - '>=21.02': [r'MyCustomFirmware 2.1(.*)'], - # OpenWrt <=21.02 configuration syntax will be used for - # these OS identifiers. - '<21.02': [r'MyCustomFirmware 2.0(.*)'] - } - } - -**Note**: The OS identifier should be a regular expression as shown in above example. - -``OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------+ -| **type**: | ``bool`` | -+--------------+----------+ -| **default**: | ``True`` | -+--------------+----------+ - -The value of this setting decides whether to use DSA syntax -(OpenWrt >=21 configuration syntax) if openwisp-controller fails -to make that decision automatically. - -``OPENWISP_CONTROLLER_GROUP_PIE_CHART`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``bool`` | -+--------------+-----------+ -| **default**: | ``False`` | -+--------------+-----------+ - -Allows to show a pie chart like the one in the screenshot. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/devicegroups-piechart.png - :alt: device groups piechart - -Active groups are groups which have at least one device in them, -while emtpy groups do not have any device assigned. - -``OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------+ -| **type**: | ``dict`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ - -.. code-block:: python - - # default value of OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS: - - dict( - max_retries=5, # total number of retries - retry_backoff=True, # exponential backoff - retry_backoff_max=600, # 10 minutes - retry_jitter=True, # randomness into exponential backoff - ) - - -This setting is utilized by background API tasks executed -by `ZeroTier VPN servers and ZeroTier VPN clients <#how-to-setup-zerotier-tunnels>`_ to handle recoverable -HTTP status codes such as 429, 500, 502, 503, and 504. These tasks are retried with a maximum -of 5 attempts with an exponential backoff and jitter, with a maximum delay of 10 minutes. - -This feature ensures that ZeroTier Service API calls -are resilient to recoverable failures, improving the reliability of the system. - -For more information on these settings, you can refer to the `the celery documentation regarding automatic retries -for known errors. `_ - - -Signals -------- - -``config_modified`` -~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.config_modified`` - -**Arguments**: - -- ``instance``: instance of ``Config`` which got its ``config`` modified -- ``previous_status``: indicates the status of the config object before the - signal was emitted -- ``action``: action which emitted the signal, can be any of the list below: - - ``config_changed``: the configuration of the config object was changed - - ``related_template_changed``: the configuration of a related template was changed - - ``m2m_templates_changed``: the assigned templates were changed - (either templates were added, removed or their order was changed) - -This signal is emitted every time the configuration of a device is modified. - -It does not matter if ``Config.status`` is already modified, this signal will -be emitted anyway because it signals that the device configuration has changed. - -This signal is used to trigger the update of the configuration on devices, -when the push feature is enabled (requires Device credentials). - -The signal is also emitted when one of the templates used by the device -is modified or if the templates assigned to the device are changed. - -Special cases in which ``config_modified`` is not emitted -######################################################### - -This signal is not emitted when the device is created for the first time. - -It is also not emitted when templates assigned to a config object are -cleared (``post_clear`` m2m signal), this is necessary because -`sortedm2m `_, the package -we use to implement ordered templates, uses the clear action to -reorder templates (m2m relationships are first cleared and then added back), -therefore we ignore ``post_clear`` to avoid emitting signals twice -(one for the clear action and one for the add action). -Please keep this in mind if you plan on using the clear method -of the m2m manager. - -``config_status_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.config_status_changed`` - -**Arguments**: - -- ``instance``: instance of ``Config`` which got its ``status`` changed - -This signal is emitted only when the configuration status of a device has changed. - -The signal is emitted also when the m2m template relationships of a config -object are changed, but only on ``post_add`` or ``post_remove`` actions, -``post_clear`` is ignored for the same reason explained -in the previous section. - -``config_backend_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.config_backend_changed`` -**Arguments**: - -- ``instance``: instance of ``Config`` which got its ``backend`` changed -- ``old_backend``: the old backend of the config object -- ``backend``: the new backend of the config object - -It is not emitted when the device or config is created. - -``checksum_requested`` -~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.checksum_requested`` - -**Arguments**: - -- ``instance``: instance of ``Device`` for which its configuration - checksum has been requested -- ``request``: the HTTP request object - -This signal is emitted when a device requests a checksum via the controller views. - -The signal is emitted just before a successful response is returned, -it is not sent if the response was not successful. - -``config_download_requested`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.config_download_requested`` - -**Arguments**: - -- ``instance``: instance of ``Device`` for which its configuration has been - requested for download -- ``request``: the HTTP request object - -This signal is emitted when a device requests to download its configuration -via the controller views. - -The signal is emitted just before a successful response is returned, -it is not sent if the response was not successful. - -``is_working_changed`` -~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.connection.signals.is_working_changed`` - -**Arguments**: - -- ``instance``: instance of ``DeviceConnection`` -- ``is_working``: value of ``DeviceConnection.is_working`` -- ``old_is_working``: previous value of ``DeviceConnection.is_working``, - either ``None`` (for new connections), ``True`` or ``False`` -- ``failure_reason``: error message explaining reason for failure in establishing connection -- ``old_failure_reason``: previous value of ``DeviceConnection.failure_reason`` - -This signal is emitted every time ``DeviceConnection.is_working`` changes. - -It is not triggered when the device is created for the first time. - -``management_ip_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.management_ip_changed`` - -**Arguments**: - -- ``instance``: instance of ``Device`` -- ``management_ip``: value of ``Device.management_ip`` -- ``old_management_ip``: previous value of ``Device.management_ip`` - -This signal is emitted every time ``Device.management_ip`` changes. - -It is not triggered when the device is created for the first time. - -``device_registered`` -~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.device_registered`` - -**Arguments**: - -- ``instance``: instance of ``Device`` which got registered. -- ``is_new``: boolean, will be ``True`` when the device is new, - ``False`` when the device already exists - (eg: a device which gets a factory reset will register again) - -This signal is emitted when a device registers automatically through the controller -HTTP API. - -``device_name_changed`` -~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.device_name_changed`` - -**Arguments**: - -- ``instance``: instance of ``Device``. - -The signal is emitted when the device name changes. - -It is not emitted when the device is created. - -``device_group_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.device_group_changed`` - -**Arguments**: - -- ``instance``: instance of ``Device``. -- ``group_id``: primary key of ``DeviceGroup`` of ``Device`` -- ``old_group_id``: primary key of previous ``DeviceGroup`` of ``Device`` - -The signal is emitted when the device group changes. - -It is not emitted when the device is created. - -``group_templates_changed`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -**Path**: ``openwisp_controller.config.signals.group_templates_changed`` - -**Arguments**: - -- ``instance``: instance of ``DeviceGroup``. -- ``templates``: list of ``Template`` objects assigned to ``DeviceGroup`` -- ``old_templates``: list of ``Template`` objects assigned earlier to ``DeviceGroup`` - -The signal is emitted when the device group templates changes. - -It is not emitted when the device is created. - -``subnet_provisioned`` -~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.subnet_division.signals.subnet_provisioned`` - -**Arguments**: - -- ``instance``: instance of ``VpnClient``. -- ``provisioned``: dictionary of ``Subnet`` and ``IpAddress`` provisioned, - ``None`` if nothing is provisioned - -The signal is emitted when subnets and IP addresses have been provisioned -for a ``VpnClient`` for a VPN server with a subnet with -`subnet division rule <#subnet-division-app>`_. - -``vpn_server_modified`` -~~~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.vpn_server_modified`` - -**Arguments**: - -- ``instance``: instance of ``Vpn``. - -The signal is emitted when the VPN server is modified. - -``vpn_peers_changed`` -~~~~~~~~~~~~~~~~~~~~~ - -**Path**: ``openwisp_controller.config.signals.vpn_peers_changed`` - -**Arguments**: - -- ``instance``: instance of ``Vpn``. - -The signal is emitted when the peers of VPN server gets changed. - -It is only emitted for ``Vpn`` object with **WireGuard** or -**VXLAN over WireGuard** backend. - -Extending openwisp-controller ------------------------------ - -One of the core values of the OpenWISP project is -`Software Reusability `_, -for this reason *openwisp-controller* 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-controller*, -you need to perform the steps described in this section. - -When in doubt, the code in the -`test project `_ -will serve you as source of truth: just replicate and adapt that code -to get a basic derivative of *openwisp-controller* working. - -If you want to add new users fields, please follow the `tutorial to extend the -openwisp-users `_. -As an example, we have extended *openwisp-users* to *sample_users* app and -added a field ``social_security_number`` in the `sample_users/models.py -`_. - -**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 project & custom apps -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Firstly, to get started you need to create a django project:: - - django-admin startproject mycontroller - -Now, you need to do is to create some new django apps which will -contain your custom version of *openwisp-controller*. - -A django project is a collection of django apps. There are 4 django apps in the -openwisp_controller project, namely config, pki, connection & geo. -You'll need to create 4 apps in your project for each app in openwisp_controller. - -A django app is nothing more than a -`python package `_ -(a directory of python scripts), in the following examples we'll call these django app -``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` -& ``sample_subnet_division``. but you can name it how you want:: - - django-admin startapp sample_config - django-admin startapp sample_pki - django-admin startapp sample_connection - django-admin startapp sample_geo - django-admin startapp sample_subnet_division - -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. - -For more information about how to work with django projects and django apps, -please refer to the `django documentation `_. - -2. Install ``openwisp-controller`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install (and add to the requirement of your project) openwisp-controller:: - - pip install openwisp-controller - -3. Add your apps in INSTALLED_APPS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now you need to add ``mycontroller.sample_config``, -``mycontroller.sample_pki``, ``mycontroller.sample_connection``, -``mycontroller.sample_geo`` & ``mycontroller.sample_subnet_division`` to -``INSTALLED_APPS`` in your ``settings.py``, ensuring also that -``openwisp_controller.config``, ``openwisp_controller.geo``, -``openwisp_controller.pki``, ``openwisp_controller.connnection`` & -``openwisp_controller.subnet_division`` have been removed: - -.. code-block:: python - - # Remember: Order in INSTALLED_APPS is important. - INSTALLED_APPS = [ - # other django installed apps - 'openwisp_utils.admin_theme', - 'admin_auto_filters', - # all-auth - 'django.contrib.sites', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - # openwisp2 module - # 'openwisp_controller.config', <-- comment out or delete this line - # 'openwisp_controller.pki', <-- comment out or delete this line - # 'openwisp_controller.geo', <-- comment out or delete this line - # 'openwisp_controller.connection', <-- comment out or delete this line - # 'openwisp_controller.subnet_division', <-- comment out or delete this line - 'mycontroller.sample_config', - 'mycontroller.sample_pki', - 'mycontroller.sample_geo', - 'mycontroller.sample_connection', - 'mycontroller.sample_subnet_division', - 'openwisp_users', - # admin - 'django.contrib.admin', - # other dependencies - 'sortedm2m', - 'reversion', - 'leaflet', - # rest framework - 'rest_framework', - 'rest_framework_gis', - # channels - 'channels', - # django-import-export - 'import_export', - ] - -Substitute ``mycontroller``, ``sample_config``, ``sample_pki``, ``sample_connection``, -``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. - -4. Add ``EXTENDED_APPS`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -Add the following to your ``settings.py``: - -.. code-block:: python - - EXTENDED_APPS = ( - 'django_x509', - 'django_loci', - 'openwisp_controller.config', - 'openwisp_controller.pki', - 'openwisp_controller.geo', - 'openwisp_controller.connection', - 'openwisp_controller.subnet_division', - ) - -5. 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', - ] - -6. Add ``openwisp_utils.loaders.DependencyLoader`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` -in your ``settings.py``, but ensure it comes before -``django.template.loaders.app_directories.Loader``: - -.. code-block:: python - - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'OPTIONS': { - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'openwisp_utils.loaders.DependencyLoader', - 'django.template.loaders.app_directories.Loader', - ], - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'openwisp_utils.admin_theme.context_processor.menu_items', - 'openwisp_notifications.context_processors.notification_api_settings', - ], - }, - } - ] - -5. Initial Database setup -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Ensure you are using one of the available geodjango backends, eg: - -.. code-block:: python - - DATABASES = { - 'default': { - 'ENGINE': 'openwisp_utils.db.backends.spatialite', - 'NAME': 'openwisp-controller.db', - } - } - -For more information about GeoDjango, please refer to the `geodjango documentation `_. - -6. Django Channels Setup -~~~~~~~~~~~~~~~~~~~~~~~~ - -Create ``asgi.py`` in your project folder and add following lines in it: - -.. code-block:: python - - from channels.auth import AuthMiddlewareStack - from channels.routing import ProtocolTypeRouter, URLRouter - from channels.security.websocket import AllowedHostsOriginValidator - from django.core.asgi import get_asgi_application - - from openwisp_controller.routing import get_routes - # You can also add your routes like this - from my_app.routing import my_routes - - application = ProtocolTypeRouter( - { "http": get_asgi_application(), - 'websocket': AllowedHostsOriginValidator( - AuthMiddlewareStack(URLRouter(get_routes() + my_routes)) - ) - } - ) - -7. Other Settings -~~~~~~~~~~~~~~~~~ - -Add the following settings to ``settings.py``: - -.. code-block:: python - - FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' - - ASGI_APPLICATION = 'my_project.asgi.application' - CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer' - }, - } - -For more information about FORM_RENDERER setting, please refer to the -`FORM_RENDERER documentation `_. -For more information about ASGI_APPLICATION setting, please refer to the -`ASGI_APPLICATION documentation `_. -For more information about CHANNEL_LAYERS setting, please refer to the -`CHANNEL_LAYERS documentation `_. - -6. Inherit the AppConfig class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Please refer to the following files in the sample app of the test project: - -- sample_config: - - `sample_config/__init__.py `_. - - `sample_config/apps.py `_. - -- sample_geo: - - `sample_geo/__init__.py `_. - - `sample_geo/apps.py `_. - -- sample_pki: - - `sample_pki/__init__.py `_. - - `sample_pki/apps.py `_. - -- sample_connection: - - `sample_connection/__init__.py `_. - - `sample_connection/apps.py `_. - -- sample_subnet_division: - - `sample_subnet_division/__init__.py `_. - - `sample_subnet_division/apps.py `_. - -You have to replicate and adapt that code in your project. - -For more information regarding the concept of ``AppConfig`` please refer to -the `"Applications" section in the django documentation `_. - -7. Create your custom models -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For the purpose of showing an example, we added a simple "details" field -to the models of the sample app in the test project. - -- `sample_config models `_ -- `sample_geo models `_ -- `sample_pki models `_ -- `sample_connection models `_ -- `sample_subnet_division `_ - -You can add fields in a similar way in your ``models.py`` file. - -**Note**: for doubts regarding how to use, extend or develop models please refer to -the `"Models" section in the django documentation `_. - -8. Add swapper configurations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Once you have created the models, add the following to your ``settings.py``: - -.. code-block:: python - - # Setting models for swapper module - CONFIG_DEVICE_MODEL = 'sample_config.Device' - CONFIG_DEVICEGROUP_MODEL = 'sample_config.DeviceGroup' - CONFIG_CONFIG_MODEL = 'sample_config.Config' - CONFIG_TEMPLATETAG_MODEL = 'sample_config.TemplateTag' - CONFIG_TAGGEDTEMPLATE_MODEL = 'sample_config.TaggedTemplate' - CONFIG_TEMPLATE_MODEL = 'sample_config.Template' - CONFIG_VPN_MODEL = 'sample_config.Vpn' - CONFIG_VPNCLIENT_MODEL = 'sample_config.VpnClient' - CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = 'sample_config.OrganizationConfigSettings' - CONFIG_ORGANIZATIONLIMITS_MODEL = 'sample_config.OrganizationLimits' - DJANGO_X509_CA_MODEL = 'sample_pki.Ca' - DJANGO_X509_CERT_MODEL = 'sample_pki.Cert' - GEO_LOCATION_MODEL = 'sample_geo.Location' - GEO_FLOORPLAN_MODEL = 'sample_geo.FloorPlan' - GEO_DEVICELOCATION_MODEL = 'sample_geo.DeviceLocation' - CONNECTION_CREDENTIALS_MODEL = 'sample_connection.Credentials' - CONNECTION_DEVICECONNECTION_MODEL = 'sample_connection.DeviceConnection' - CONNECTION_COMMAND_MODEL = 'sample_connection.Command' - SUBNET_DIVISION_SUBNETDIVISIONRULE_MODEL = 'sample_subnet_division.SubnetDivisionRule' - SUBNET_DIVISION_SUBNETDIVISIONINDEX_MODEL = 'sample_subnet_division.SubnetDivisionIndex' - -Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, -``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. - -9. Create database migrations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Create database migrations:: - - ./manage.py makemigrations - -Now, to use the default ``administrator`` and ``operator`` user groups -like the used in the openwisp_controller module, you'll manually need to make a -migrations file which would look like: - -- `sample_config/migrations/0002_default_groups_permissions.py `_ -- `sample_geo/migrations/0002_default_group_permissions.py `_ -- `sample_pki/migrations/0002_default_group_permissions.py `_ -- `sample_connection/migrations/0002_default_group_permissions.py `_ -- `sample_subnet_division/migrations/0002_default_group_permissions.py `_ - -Create database migrations:: - - ./manage.py migrate - -For more information, refer to the -`"Migrations" section in the django documentation `_. - -10. Create the admin -~~~~~~~~~~~~~~~~~~~~ - -Refer to the ``admin.py`` file of the sample app. - -- `sample_config admin.py `_. -- `sample_geo admin.py `_. -- `sample_pki admin.py `_. -- `sample_connection admin.py `_. -- `sample_subnet_division admin.py `_. - -To introduce changes to the admin, you can do it in two main ways which are described below. - -**Note**: for more information 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: - -sample_config -^^^^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.config.admin import ( - DeviceAdmin, - DeviceGroupAdmin, - TemplateAdmin, - VpnAdmin, - ) - - # DeviceAdmin.fields += ['example'] <-- monkey patching example - -sample_connection -^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.connection.admin import CredentialsAdmin - - # CredentialsAdmin.fields += ['example'] <-- monkey patching example - -sample_geo -^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.geo.admin import FloorPlanAdmin, LocationAdmin - - # FloorPlanAdmin.fields += ['example'] <-- monkey patching example - -sample_pki -^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.pki.admin import CaAdmin, CertAdmin - - # CaAdmin.fields += ['example'] <-- monkey patching example - -sample_subnet_division -^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.subnet_division.admin import SubnetDivisionRuleInlineAdmin - - # SubnetDivisionRuleInlineAdmin.fields += ['example'] <-- monkey patching example - -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: - -sample_config -^^^^^^^^^^^^^ - -.. code-block:: python - - from django.contrib import admin - from openwisp_controller.config.admin import ( - DeviceAdmin as BaseDeviceAdmin, - TemplateAdmin as BaseTemplateAdmin, - VpnAdmin as BaseVpnAdmin, - DeviceGroupAdmin as BaseDeviceGroupAdmin, - from swapper import load_model - - Vpn = load_model('openwisp_controller', 'Vpn') - Device = load_model('openwisp_controller', 'Device') - DeviceGroup = load_model('openwisp_controller', 'DeviceGroup') - Template = load_model('openwisp_controller', 'Template') - - admin.site.unregister(Vpn) - admin.site.unregister(Device) - admin.site.unregister(DeviceGroup) - admin.site.unregister(Template) - - @admin.register(Vpn) - class VpnAdmin(BaseVpnAdmin): - # add your changes here - - @admin.register(Device) - class DeviceAdmin(BaseDeviceAdmin): - # add your changes here - - @admin.register(DeviceGroup) - class DeviceGroupAdmin(BaseDeviceGroupAdmin): - # add your changes here - - @admin.register(Template) - class TemplateAdmin(BaseTemplateAdmin): - # add your changes here - -sample_connection -^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.connection.admin import CredentialsAdmin as BaseCredentialsAdmin - from django.contrib import admin - from swapper import load_model - - Credentials = load_model('openwisp_controller', 'Credentials') - - admin.site.unregister(Credentials) - - @admin.register(Device) - class CredentialsAdmin(BaseCredentialsAdmin): - # add your changes here - -sample_geo -^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.geo.admin import ( - FloorPlanAdmin as BaseFloorPlanAdmin, - LocationAdmin as BaseLocationAdmin - ) - from django.contrib import admin - from swapper import load_model - - Location = load_model('openwisp_controller', 'Location') - FloorPlan = load_model('openwisp_controller', 'FloorPlan') - - admin.site.unregister(FloorPlan) - admin.site.unregister(Location) - - @admin.register(FloorPlan) - class FloorPlanAdmin(BaseFloorPlanAdmin): - # add your changes here - - @admin.register(Location) - class LocationAdmin(BaseLocationAdmin): - # add your changes here - -sample_pki -^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.geo.admin import ( - CaAdmin as BaseCaAdmin, - CertAdmin as BaseCertAdmin - ) - from django.contrib import admin - from swapper import load_model - - Ca = load_model('openwisp_controller', 'Ca') - Cert = load_model('openwisp_controller', 'Cert') - - admin.site.unregister(Ca) - admin.site.unregister(Cert) - - @admin.register(Ca) - class CaAdmin(BaseCaAdmin): - # add your changes here - - @admin.register(Cert) - class CertAdmin(BaseCertAdmin): - # add your changes here - -sample_subnet_division -^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - from openwisp_controller.subnet_division.admin import ( - SubnetAdmin as BaseSubnetAdmin, - IpAddressAdmin as BaseIpAddressAdmin, - SubnetDivisionRuleInlineAdmin as BaseSubnetDivisionRuleInlineAdmin, - ) - from django.contrib import admin - from swapper import load_model - - Subnet = load_model('openwisp_ipam', 'Subnet') - IpAddress = load_model('openwisp_ipam', 'IpAddress') - SubnetDivisionRule = load_model('subnet_division', 'SubnetDivisionRule') - - admin.site.unregister(Subnet) - admin.site.unregister(IpAddress) - admin.site.unregister(SubnetDivisionRule) - - @admin.register(Subnet) - class SubnetAdmin(BaseSubnetAdmin): - # add your changes here - - @admin.register(IpAddress) - class IpAddressAdmin(BaseIpAddressAdmin): - # add your changes here - - @admin.register(SubnetDivisionRule) - class SubnetDivisionRuleInlineAdmin(BaseSubnetDivisionRuleInlineAdmin): - # add your changes here - -11. Create root URL configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from django.contrib import admin - from openwisp_controller.config.utils import get_controller_urls - from openwisp_controller.geo.utils import get_geo_urls - # from .sample_config import views as config_views - # from .sample_geo import views as geo_views - - urlpatterns = [ - # ... other urls in your project ... - # Use only when changing controller API views (discussed below) - # url(r'^controller/', include((get_controller_urls(config_views), 'controller'), namespace='controller')) - - # Use only when changing geo API views (discussed below) - # url(r'^geo/', include((get_geo_urls(geo_views), 'geo'), namespace='geo')), - - # openwisp-controller urls - url(r'', include(('openwisp_controller.config.urls', 'config'), namespace='config')), - url(r'', include('openwisp_controller.urls')), - ] - -For more information about URL configuration in django, please refer to the -`"URL dispatcher" section in the django documentation `_. - -12. 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-controller*. - -In case you need to add breaking changes, you can overwrite the tests defined -in the base classes to test your own behavior. - -See the tests in sample_app to find out how to do this. - -- `project common tests.py `_ -- `sample_config tests.py `_ -- `sample_geo tests.py `_ -- `sample_geo pytest.py `_ -- `sample_pki tests.py `_ -- `sample_connection tests.py `_ -- `sample_subnet_division tests.py `_ - -For running the tests, you need to copy fixtures as well: - -- Change `sample_config` to your config app's name in `sample_config fixtures `_ and paste it in the ``sample_config/fixtures/`` directory. - -You can then run tests with:: - - # the --parallel flag is optional - ./manage.py test --parallel mycontroller - -Substitute ``mycontroller`` with the name you chose in step 1. - -For more information about automated tests in django, please refer to -`"Testing in Django" `_. - -Other base classes that can be inherited and extended -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following steps are not required and are intended for more advanced customization. - -1. Extending the Controller API Views -##################################### - -Extending the `sample_config/views.py `_ -is required only when you want to make changes in the controller API, -Remember to change ``config_views`` location in ``urls.py`` in point 11 for extending views. - -For more information about django views, please refer to the `views section in the django documentation `_. - -2. Extending the Geo API Views -############################## - -Extending the `sample_geo/views.py `_ -is required only when you want to make changes in the geo API, -Remember to change ``geo_views`` location in ``urls.py`` in point 11 for extending views. - -For more information about django views, please refer to the `views section in the django documentation `_. - -Custom Subnet Division Rule Types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It is possible to create your own `subnet division rule types <#subnet-division-app>`_. -The rule type determines when subnets and IPs will be provisioned and when they -will be destroyed. - -You can create your custom rule types by extending -``openwisp_controller.subnet_division.rule_types.base.BaseSubnetDivisionRuleType``. - -Below is an example to create a subnet division rule type that will provision -subnets and IPs when a new device is created and will delete them upon deletion -for that device. - -.. code-block:: python - - # In mycontroller/sample_subnet_division/rules_types/custom.py - - from django.db.models.signals import post_delete, post_save - from swapper import load_model - - from openwisp_controller.subnet_division.rule_types.base import ( - BaseSubnetDivisionRuleType, - ) - - Device = load_model('config', 'Device') - - class CustomRuleType(BaseSubnetDivisionRuleType): - # The signal on which provisioning should be triggered - provision_signal = post_save - # The sender of the provision_signal - provision_sender = Device - # Dispatch UID for connecting provision_signal to provision_receiver - provision_dispatch_uid = 'some_unique_identifier_string' - - # The signal on which deletion should be triggered - destroyer_signal = post_delete - # The sender of the destroyer_signal - destroyer_sender = Device - # Dispatch UID for connecting destroyer_signal to destroyer_receiver - destroyer_dispatch_uid = 'another_unique_identifier_string' - - # Attribute path to organization_id - # Example 1: If organization_id is direct attribute of provision_signal - # sender instance, then - # organization_id_path = 'organization_id' - # Example 2: If organization_id is indirect attribute of provision signal - # sender instance, then - # organization_id_path = 'some_attribute.another_intermediate.organization_id' - organization_id_path = 'organization_id' - - # Similar to organization_id_path but for the required subnet attribute - subnet_path = 'subnet' - - # An intermediate method through which you can specify conditions for provisions - @classmethod - def should_create_subnets_ips(cls, instance, **kwargs): - # Using "post_save" provision_signal, the rule should be only - # triggered when a new object is created. - return kwargs['created'] - - # You can define logic to trigger provisioning for existing objects - # using following classmethod. By default, BaseSubnetDivisionRuleType - # performs no operation for existing objects. - @classmethod - def provision_for_existing_objects(cls, rule_obj): - for device in Device.objects.filter( - organization=rule_obj.organization - ): - cls.provision_receiver(device, created=True) - -After creating a class for your custom rule type, you will need to set -`OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES <#openwisp-controller-subnet-division-types>`_ -setting as follows: - -.. code-block:: python - - OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES = ( | - ('openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType', 'VPN'), - ('openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType', 'Device'), - ('mycontroller.sample_subnet_division.rules_types.custom.CustomRuleType', 'Custom Rule'), - ) - -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 `documentation section about -registering notification types 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. - Contributing ------------ diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst new file mode 100644 index 000000000..5405e7d0c --- /dev/null +++ b/docs/developer/developer-docs.rst @@ -0,0 +1,12 @@ +Developers Documentation +------------------------ + +.. include:: /paritals/developers-docs-warning.rst + +.. toctree:: + :maxdepth: 1 + + ./installation.rst + ./project-structure.rst + ./signals.rst + ./extending.rst diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst new file mode 100644 index 000000000..f7334beea --- /dev/null +++ b/docs/developer/extending.rst @@ -0,0 +1,749 @@ +Extending openwisp-controller +----------------------------- + +.. include:: /paritals/developers-docs-warning.rst + +One of the core values of the OpenWISP project is +`Software Reusability `_, +for this reason *openwisp-controller* 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-controller*, +you need to perform the steps described in this section. + +When in doubt, the code in the +`test project `_ +will serve you as source of truth: just replicate and adapt that code +to get a basic derivative of *openwisp-controller* working. + +If you want to add new users fields, please follow the `tutorial to extend the +openwisp-users `_. +As an example, we have extended *openwisp-users* to *sample_users* app and +added a field ``social_security_number`` in the `sample_users/models.py +`_. + +**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 project & custom apps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Firstly, to get started you need to create a django project:: + + django-admin startproject mycontroller + +Now, you need to do is to create some new django apps which will +contain your custom version of *openwisp-controller*. + +A django project is a collection of django apps. There are 4 django apps in the +openwisp_controller project, namely config, pki, connection & geo. +You'll need to create 4 apps in your project for each app in openwisp_controller. + +A django app is nothing more than a +`python package `_ +(a directory of python scripts), in the following examples we'll call these django app +``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` +& ``sample_subnet_division``. but you can name it how you want:: + + django-admin startapp sample_config + django-admin startapp sample_pki + django-admin startapp sample_connection + django-admin startapp sample_geo + django-admin startapp sample_subnet_division + +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. + +For more information about how to work with django projects and django apps, +please refer to the `django documentation `_. + +2. Install ``openwisp-controller`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install (and add to the requirement of your project) openwisp-controller:: + + pip install openwisp-controller + +3. Add your apps in INSTALLED_APPS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now you need to add ``mycontroller.sample_config``, +``mycontroller.sample_pki``, ``mycontroller.sample_connection``, +``mycontroller.sample_geo`` & ``mycontroller.sample_subnet_division`` to +``INSTALLED_APPS`` in your ``settings.py``, ensuring also that +``openwisp_controller.config``, ``openwisp_controller.geo``, +``openwisp_controller.pki``, ``openwisp_controller.connnection`` & +``openwisp_controller.subnet_division`` have been removed: + +.. code-block:: python + + # Remember: Order in INSTALLED_APPS is important. + INSTALLED_APPS = [ + # other django installed apps + 'openwisp_utils.admin_theme', + 'admin_auto_filters', + # all-auth + 'django.contrib.sites', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + # openwisp2 module + # 'openwisp_controller.config', <-- comment out or delete this line + # 'openwisp_controller.pki', <-- comment out or delete this line + # 'openwisp_controller.geo', <-- comment out or delete this line + # 'openwisp_controller.connection', <-- comment out or delete this line + # 'openwisp_controller.subnet_division', <-- comment out or delete this line + 'mycontroller.sample_config', + 'mycontroller.sample_pki', + 'mycontroller.sample_geo', + 'mycontroller.sample_connection', + 'mycontroller.sample_subnet_division', + 'openwisp_users', + # admin + 'django.contrib.admin', + # other dependencies + 'sortedm2m', + 'reversion', + 'leaflet', + # rest framework + 'rest_framework', + 'rest_framework_gis', + # channels + 'channels', + # django-import-export + 'import_export', + ] + +Substitute ``mycontroller``, ``sample_config``, ``sample_pki``, ``sample_connection``, +``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. + +4. Add ``EXTENDED_APPS`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Add the following to your ``settings.py``: + +.. code-block:: python + + EXTENDED_APPS = ( + 'django_x509', + 'django_loci', + 'openwisp_controller.config', + 'openwisp_controller.pki', + 'openwisp_controller.geo', + 'openwisp_controller.connection', + 'openwisp_controller.subnet_division', + ) + +5. 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', + ] + +6. Add ``openwisp_utils.loaders.DependencyLoader`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` +in your ``settings.py``, but ensure it comes before +``django.template.loaders.app_directories.Loader``: + +.. code-block:: python + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'OPTIONS': { + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'openwisp_utils.loaders.DependencyLoader', + 'django.template.loaders.app_directories.Loader', + ], + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'openwisp_utils.admin_theme.context_processor.menu_items', + 'openwisp_notifications.context_processors.notification_api_settings', + ], + }, + } + ] + +5. Initial Database setup +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ensure you are using one of the available geodjango backends, eg: + +.. code-block:: python + + DATABASES = { + 'default': { + 'ENGINE': 'openwisp_utils.db.backends.spatialite', + 'NAME': 'openwisp-controller.db', + } + } + +For more information about GeoDjango, please refer to the `geodjango documentation `_. + +6. Django Channels Setup +~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``asgi.py`` in your project folder and add following lines in it: + +.. code-block:: python + + from channels.auth import AuthMiddlewareStack + from channels.routing import ProtocolTypeRouter, URLRouter + from channels.security.websocket import AllowedHostsOriginValidator + from django.core.asgi import get_asgi_application + + from openwisp_controller.routing import get_routes + # You can also add your routes like this + from my_app.routing import my_routes + + application = ProtocolTypeRouter( + { "http": get_asgi_application(), + 'websocket': AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(get_routes() + my_routes)) + ) + } + ) + +7. Other Settings +~~~~~~~~~~~~~~~~~ + +Add the following settings to ``settings.py``: + +.. code-block:: python + + FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + + ASGI_APPLICATION = 'my_project.asgi.application' + CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels.layers.InMemoryChannelLayer' + }, + } + +For more information about FORM_RENDERER setting, please refer to the +`FORM_RENDERER documentation `_. +For more information about ASGI_APPLICATION setting, please refer to the +`ASGI_APPLICATION documentation `_. +For more information about CHANNEL_LAYERS setting, please refer to the +`CHANNEL_LAYERS documentation `_. + +6. Inherit the AppConfig class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please refer to the following files in the sample app of the test project: + +- sample_config: + - `sample_config/__init__.py `_. + - `sample_config/apps.py `_. + +- sample_geo: + - `sample_geo/__init__.py `_. + - `sample_geo/apps.py `_. + +- sample_pki: + - `sample_pki/__init__.py `_. + - `sample_pki/apps.py `_. + +- sample_connection: + - `sample_connection/__init__.py `_. + - `sample_connection/apps.py `_. + +- sample_subnet_division: + - `sample_subnet_division/__init__.py `_. + - `sample_subnet_division/apps.py `_. + +You have to replicate and adapt that code in your project. + +For more information regarding the concept of ``AppConfig`` please refer to +the `"Applications" section in the django documentation `_. + +7. Create your custom models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For the purpose of showing an example, we added a simple "details" field +to the models of the sample app in the test project. + +- `sample_config models `_ +- `sample_geo models `_ +- `sample_pki models `_ +- `sample_connection models `_ +- `sample_subnet_division `_ + +You can add fields in a similar way in your ``models.py`` file. + +**Note**: for doubts regarding how to use, extend or develop models please refer to +the `"Models" section in the django documentation `_. + +8. Add swapper configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you have created the models, add the following to your ``settings.py``: + +.. code-block:: python + + # Setting models for swapper module + CONFIG_DEVICE_MODEL = 'sample_config.Device' + CONFIG_DEVICEGROUP_MODEL = 'sample_config.DeviceGroup' + CONFIG_CONFIG_MODEL = 'sample_config.Config' + CONFIG_TEMPLATETAG_MODEL = 'sample_config.TemplateTag' + CONFIG_TAGGEDTEMPLATE_MODEL = 'sample_config.TaggedTemplate' + CONFIG_TEMPLATE_MODEL = 'sample_config.Template' + CONFIG_VPN_MODEL = 'sample_config.Vpn' + CONFIG_VPNCLIENT_MODEL = 'sample_config.VpnClient' + CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = 'sample_config.OrganizationConfigSettings' + CONFIG_ORGANIZATIONLIMITS_MODEL = 'sample_config.OrganizationLimits' + DJANGO_X509_CA_MODEL = 'sample_pki.Ca' + DJANGO_X509_CERT_MODEL = 'sample_pki.Cert' + GEO_LOCATION_MODEL = 'sample_geo.Location' + GEO_FLOORPLAN_MODEL = 'sample_geo.FloorPlan' + GEO_DEVICELOCATION_MODEL = 'sample_geo.DeviceLocation' + CONNECTION_CREDENTIALS_MODEL = 'sample_connection.Credentials' + CONNECTION_DEVICECONNECTION_MODEL = 'sample_connection.DeviceConnection' + CONNECTION_COMMAND_MODEL = 'sample_connection.Command' + SUBNET_DIVISION_SUBNETDIVISIONRULE_MODEL = 'sample_subnet_division.SubnetDivisionRule' + SUBNET_DIVISION_SUBNETDIVISIONINDEX_MODEL = 'sample_subnet_division.SubnetDivisionIndex' + +Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, +``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. + +9. Create database migrations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create database migrations:: + + ./manage.py makemigrations + +Now, to use the default ``administrator`` and ``operator`` user groups +like the used in the openwisp_controller module, you'll manually need to make a +migrations file which would look like: + +- `sample_config/migrations/0002_default_groups_permissions.py `_ +- `sample_geo/migrations/0002_default_group_permissions.py `_ +- `sample_pki/migrations/0002_default_group_permissions.py `_ +- `sample_connection/migrations/0002_default_group_permissions.py `_ +- `sample_subnet_division/migrations/0002_default_group_permissions.py `_ + +Create database migrations:: + + ./manage.py migrate + +For more information, refer to the +`"Migrations" section in the django documentation `_. + +10. Create the admin +~~~~~~~~~~~~~~~~~~~~ + +Refer to the ``admin.py`` file of the sample app. + +- `sample_config admin.py `_. +- `sample_geo admin.py `_. +- `sample_pki admin.py `_. +- `sample_connection admin.py `_. +- `sample_subnet_division admin.py `_. + +To introduce changes to the admin, you can do it in two main ways which are described below. + +**Note**: for more information 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: + +sample_config +^^^^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.config.admin import ( + DeviceAdmin, + DeviceGroupAdmin, + TemplateAdmin, + VpnAdmin, + ) + + # DeviceAdmin.fields += ['example'] <-- monkey patching example + +sample_connection +^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.connection.admin import CredentialsAdmin + + # CredentialsAdmin.fields += ['example'] <-- monkey patching example + +sample_geo +^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.geo.admin import FloorPlanAdmin, LocationAdmin + + # FloorPlanAdmin.fields += ['example'] <-- monkey patching example + +sample_pki +^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.pki.admin import CaAdmin, CertAdmin + + # CaAdmin.fields += ['example'] <-- monkey patching example + +sample_subnet_division +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.subnet_division.admin import SubnetDivisionRuleInlineAdmin + + # SubnetDivisionRuleInlineAdmin.fields += ['example'] <-- monkey patching example + +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: + +sample_config +^^^^^^^^^^^^^ + +.. code-block:: python + + from django.contrib import admin + from openwisp_controller.config.admin import ( + DeviceAdmin as BaseDeviceAdmin, + TemplateAdmin as BaseTemplateAdmin, + VpnAdmin as BaseVpnAdmin, + DeviceGroupAdmin as BaseDeviceGroupAdmin, + from swapper import load_model + + Vpn = load_model('openwisp_controller', 'Vpn') + Device = load_model('openwisp_controller', 'Device') + DeviceGroup = load_model('openwisp_controller', 'DeviceGroup') + Template = load_model('openwisp_controller', 'Template') + + admin.site.unregister(Vpn) + admin.site.unregister(Device) + admin.site.unregister(DeviceGroup) + admin.site.unregister(Template) + + @admin.register(Vpn) + class VpnAdmin(BaseVpnAdmin): + # add your changes here + + @admin.register(Device) + class DeviceAdmin(BaseDeviceAdmin): + # add your changes here + + @admin.register(DeviceGroup) + class DeviceGroupAdmin(BaseDeviceGroupAdmin): + # add your changes here + + @admin.register(Template) + class TemplateAdmin(BaseTemplateAdmin): + # add your changes here + +sample_connection +^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.connection.admin import CredentialsAdmin as BaseCredentialsAdmin + from django.contrib import admin + from swapper import load_model + + Credentials = load_model('openwisp_controller', 'Credentials') + + admin.site.unregister(Credentials) + + @admin.register(Device) + class CredentialsAdmin(BaseCredentialsAdmin): + # add your changes here + +sample_geo +^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.geo.admin import ( + FloorPlanAdmin as BaseFloorPlanAdmin, + LocationAdmin as BaseLocationAdmin + ) + from django.contrib import admin + from swapper import load_model + + Location = load_model('openwisp_controller', 'Location') + FloorPlan = load_model('openwisp_controller', 'FloorPlan') + + admin.site.unregister(FloorPlan) + admin.site.unregister(Location) + + @admin.register(FloorPlan) + class FloorPlanAdmin(BaseFloorPlanAdmin): + # add your changes here + + @admin.register(Location) + class LocationAdmin(BaseLocationAdmin): + # add your changes here + +sample_pki +^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.geo.admin import ( + CaAdmin as BaseCaAdmin, + CertAdmin as BaseCertAdmin + ) + from django.contrib import admin + from swapper import load_model + + Ca = load_model('openwisp_controller', 'Ca') + Cert = load_model('openwisp_controller', 'Cert') + + admin.site.unregister(Ca) + admin.site.unregister(Cert) + + @admin.register(Ca) + class CaAdmin(BaseCaAdmin): + # add your changes here + + @admin.register(Cert) + class CertAdmin(BaseCertAdmin): + # add your changes here + +sample_subnet_division +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from openwisp_controller.subnet_division.admin import ( + SubnetAdmin as BaseSubnetAdmin, + IpAddressAdmin as BaseIpAddressAdmin, + SubnetDivisionRuleInlineAdmin as BaseSubnetDivisionRuleInlineAdmin, + ) + from django.contrib import admin + from swapper import load_model + + Subnet = load_model('openwisp_ipam', 'Subnet') + IpAddress = load_model('openwisp_ipam', 'IpAddress') + SubnetDivisionRule = load_model('subnet_division', 'SubnetDivisionRule') + + admin.site.unregister(Subnet) + admin.site.unregister(IpAddress) + admin.site.unregister(SubnetDivisionRule) + + @admin.register(Subnet) + class SubnetAdmin(BaseSubnetAdmin): + # add your changes here + + @admin.register(IpAddress) + class IpAddressAdmin(BaseIpAddressAdmin): + # add your changes here + + @admin.register(SubnetDivisionRule) + class SubnetDivisionRuleInlineAdmin(BaseSubnetDivisionRuleInlineAdmin): + # add your changes here + +11. Create root URL configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from django.contrib import admin + from openwisp_controller.config.utils import get_controller_urls + from openwisp_controller.geo.utils import get_geo_urls + # from .sample_config import views as config_views + # from .sample_geo import views as geo_views + + urlpatterns = [ + # ... other urls in your project ... + # Use only when changing controller API views (discussed below) + # url(r'^controller/', include((get_controller_urls(config_views), 'controller'), namespace='controller')) + + # Use only when changing geo API views (discussed below) + # url(r'^geo/', include((get_geo_urls(geo_views), 'geo'), namespace='geo')), + + # openwisp-controller urls + url(r'', include(('openwisp_controller.config.urls', 'config'), namespace='config')), + url(r'', include('openwisp_controller.urls')), + ] + +For more information about URL configuration in django, please refer to the +`"URL dispatcher" section in the django documentation `_. + +12. 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-controller*. + +In case you need to add breaking changes, you can overwrite the tests defined +in the base classes to test your own behavior. + +See the tests in sample_app to find out how to do this. + +- `project common tests.py `_ +- `sample_config tests.py `_ +- `sample_geo tests.py `_ +- `sample_geo pytest.py `_ +- `sample_pki tests.py `_ +- `sample_connection tests.py `_ +- `sample_subnet_division tests.py `_ + +For running the tests, you need to copy fixtures as well: + +- Change `sample_config` to your config app's name in `sample_config fixtures `_ and paste it in the ``sample_config/fixtures/`` directory. + +You can then run tests with:: + + # the --parallel flag is optional + ./manage.py test --parallel mycontroller + +Substitute ``mycontroller`` with the name you chose in step 1. + +For more information about automated tests in django, please refer to +`"Testing in Django" `_. + +Other base classes that can be inherited and extended +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following steps are not required and are intended for more advanced customization. + +1. Extending the Controller API Views +##################################### + +Extending the `sample_config/views.py `_ +is required only when you want to make changes in the controller API, +Remember to change ``config_views`` location in ``urls.py`` in point 11 for extending views. + +For more information about django views, please refer to the `views section in the django documentation `_. + +2. Extending the Geo API Views +############################## + +Extending the `sample_geo/views.py `_ +is required only when you want to make changes in the geo API, +Remember to change ``geo_views`` location in ``urls.py`` in point 11 for extending views. + +For more information about django views, please refer to the `views section in the django documentation `_. + +Custom Subnet Division Rule Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is possible to create your own `subnet division rule types <#subnet-division-app>`_. +The rule type determines when subnets and IPs will be provisioned and when they +will be destroyed. + +You can create your custom rule types by extending +``openwisp_controller.subnet_division.rule_types.base.BaseSubnetDivisionRuleType``. + +Below is an example to create a subnet division rule type that will provision +subnets and IPs when a new device is created and will delete them upon deletion +for that device. + +.. code-block:: python + + # In mycontroller/sample_subnet_division/rules_types/custom.py + + from django.db.models.signals import post_delete, post_save + from swapper import load_model + + from openwisp_controller.subnet_division.rule_types.base import ( + BaseSubnetDivisionRuleType, + ) + + Device = load_model('config', 'Device') + + class CustomRuleType(BaseSubnetDivisionRuleType): + # The signal on which provisioning should be triggered + provision_signal = post_save + # The sender of the provision_signal + provision_sender = Device + # Dispatch UID for connecting provision_signal to provision_receiver + provision_dispatch_uid = 'some_unique_identifier_string' + + # The signal on which deletion should be triggered + destroyer_signal = post_delete + # The sender of the destroyer_signal + destroyer_sender = Device + # Dispatch UID for connecting destroyer_signal to destroyer_receiver + destroyer_dispatch_uid = 'another_unique_identifier_string' + + # Attribute path to organization_id + # Example 1: If organization_id is direct attribute of provision_signal + # sender instance, then + # organization_id_path = 'organization_id' + # Example 2: If organization_id is indirect attribute of provision signal + # sender instance, then + # organization_id_path = 'some_attribute.another_intermediate.organization_id' + organization_id_path = 'organization_id' + + # Similar to organization_id_path but for the required subnet attribute + subnet_path = 'subnet' + + # An intermediate method through which you can specify conditions for provisions + @classmethod + def should_create_subnets_ips(cls, instance, **kwargs): + # Using "post_save" provision_signal, the rule should be only + # triggered when a new object is created. + return kwargs['created'] + + # You can define logic to trigger provisioning for existing objects + # using following classmethod. By default, BaseSubnetDivisionRuleType + # performs no operation for existing objects. + @classmethod + def provision_for_existing_objects(cls, rule_obj): + for device in Device.objects.filter( + organization=rule_obj.organization + ): + cls.provision_receiver(device, created=True) + +After creating a class for your custom rule type, you will need to set +`OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES <#openwisp-controller-subnet-division-types>`_ +setting as follows: + +.. code-block:: python + + OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES = ( | + ('openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType', 'VPN'), + ('openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType', 'Device'), + ('mycontroller.sample_subnet_division.rules_types.custom.CustomRuleType', 'Custom Rule'), + ) + +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 `documentation section about +registering notification types 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/installation.rst b/docs/developer/installation.rst new file mode 100644 index 000000000..28208770f --- /dev/null +++ b/docs/developer/installation.rst @@ -0,0 +1,176 @@ +Developer installation instructions +----------------------------------- + +.. include:: /paritals/developers-docs-warning.rst + +Dependencies +~~~~~~~~~~~~ + +* Python >= 3.7 +* OpenSSL + +Install stable version from pypi +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install from pypi: + +.. code-block:: shell + + pip install openwisp-controller + +Install development version +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install tarball: + +.. code-block:: shell + + pip install https://github.com/openwisp/openwisp-controller/tarball/master + +Alternatively you can install via pip using git: + +.. code-block:: shell + + pip install -e git+git://github.com/openwisp/openwisp-controller#egg=openwisp_controller + +If you want to contribute, follow the instructions in +`Installing for development <#installing-for-development>`_. + +Installing for development +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install the system dependencies: + +.. code-block:: shell + + 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 chromium + +Fork and clone the forked repository: + +.. code-block:: shell + + git clone git://github.com//openwisp-controller + +Navigate into the cloned repository: + +.. code-block:: shell + + cd openwisp-controller/ + +Launch Redis and PostgreSQL: + +.. code-block:: shell + + docker-compose up -d redis postgres + +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 + +Launch celery worker (for background jobs): + +.. code-block:: shell + + celery -A openwisp2 worker -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 --parallel + # To run database tests against PostgreSQL backend + POSTGRESQL=1 ./runtests.py --parallel + +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 + +Troubleshooting steps for common installation issues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You may encounter some issues while installing GeoDjango. + +Unable to load SpatiaLite library extension? +############################################ + +If you are getting below exception:: + + django.core.exceptions.ImproperlyConfigured: Unable to load the SpatiaLite library extension + +then, You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as explained in +`django documentation regarding how to install and configure spatialte +`_. + +Having Issues with other geospatial libraries? +############################################## + +Please refer +`troubleshooting issues related to geospatial libraries +`_. + +.. important:: + + If you want to add ``openwisp-controller`` in an existing Django + project, then you can take reference from the + `test project in openwisp-controller repository + `_ diff --git a/docs/developer/project-structure.rst b/docs/developer/project-structure.rst new file mode 100644 index 000000000..0b4d4c6b1 --- /dev/null +++ b/docs/developer/project-structure.rst @@ -0,0 +1,96 @@ +Project Structure & main features +---------------------------------- + +.. include:: /paritals/developers-docs-warning.rst + +OpenWISP Controller is a python package consisting of four django apps: + +Config App +~~~~~~~~~~ + +* **configuration management** for embedded devices supporting different firmwares: + - `OpenWRT `_ + - `OpenWISP Firmware `_ + - support for additional firmware can be added by `specifying custom backends <#netjsonconfig-backends>`_ +* **configuration editor** based on `JSON-Schema editor `_ +* **advanced edit mode**: edit `NetJSON `_ *DeviceConfiguration* objects for maximum flexibility +* `configuration templates `_: + reduce repetition to the minimum, configure default and required templates +* `configuration variables <#how-to-use-configuration-variables>`_: + reference ansible-like variables in the configuration and templates +* **template tags**: tag templates to automate different types of auto-configurations (eg: mesh, WDS, 4G) +* **device groups**: add `devices to dedicated groups <#device-groups>`_ to + ease management of group of devices +* **simple HTTP resources**: allow devices to automatically download configuration updates +* **VPN management**: `automatically provision VPN tunnels <#openwisp-controller-default-auto-cert>`_, + including cryptographic keys, IP addresses +* `REST API <#rest-api-reference>`_ +* `Export/Import devices <#>`_ + +PKI App +~~~~~~~ + +The PKI app is based on `django-x509 `_, +it allows to create, import and view x509 CAs and certificates directly from +the administration dashboard, it also adds different endpoints to the +`REST API <#rest-api-reference>`_. + +Connection App +~~~~~~~~~~~~~~ + +This app allows OpenWISP Controller to use different protocols to reach network devices. +Currently, the default connnection protocols are SSH and SNMP, but the protocol +mechanism is extensible and more protocols can be implemented if needed. + +SSH +### + +The SSH connector allows the controller to initialize connections to the devices +in order perform `push operations <#how-to-configure-push-updates>`__: + +- Sending configuration updates. +- `Executing shell commands <#sending-commands-to-devices>`_. +- Perform `firmware upgrades via the additional firmware upgrade module `_. +- `REST API <#rest-api-reference>`_ + +The default connection protocol implemented is SSH, but other protocol +mechanism is extensible and custom protocols can be implemented as well. + +Access via SSH key is recommended, the SSH key algorithms supported are: + +- RSA +- Ed25519 + +SNMP +#### + +The SNMP connector is useful to collect monitoring information and it's used in +`openwisp-monitoring`_ for performing checks to collect monitoring information. +`Read more `_ on how to use it. + +Geo App +~~~~~~~ + +The geographic app is based on `django-loci `_ +and allows to define the geographic coordinates of the devices, +as well as their indoor coordinates on floorplan images. + +It also adds different endpoints to the `REST API <#rest-api-reference>`_. + +Subnet Division App +~~~~~~~~~~~~~~~~~~~ + +This app allows to automatically provision subnets and IP addresses which will be +available as `system defined configuration variables <#system-defined-variables>`_ +that can be used in templates. The purpose of this app is to allow users to automatically +provision and configure specific +subnets and IP addresses to the devices without the need of manual intervention. + +Refer to `"How to configure automatic provisioning of subnets and IPs" +section of this documentation +<#how-to-configure-automatic-provisioning-of-subnets-and-ips>`_ +to learn about features provided by this app. + +This app is optional, if you don't need it you can avoid adding it to +``settings.INSTALLED_APPS``. + diff --git a/docs/developer/signals.rst b/docs/developer/signals.rst new file mode 100644 index 000000000..568956a84 --- /dev/null +++ b/docs/developer/signals.rst @@ -0,0 +1,239 @@ +Signals +------- + +.. include:: /paritals/developers-docs-warning.rst + +``config_modified`` +~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.config_modified`` + +**Arguments**: + +- ``instance``: instance of ``Config`` which got its ``config`` modified +- ``previous_status``: indicates the status of the config object before the + signal was emitted +- ``action``: action which emitted the signal, can be any of the list below: + - ``config_changed``: the configuration of the config object was changed + - ``related_template_changed``: the configuration of a related template was changed + - ``m2m_templates_changed``: the assigned templates were changed + (either templates were added, removed or their order was changed) + +This signal is emitted every time the configuration of a device is modified. + +It does not matter if ``Config.status`` is already modified, this signal will +be emitted anyway because it signals that the device configuration has changed. + +This signal is used to trigger the update of the configuration on devices, +when the push feature is enabled (requires Device credentials). + +The signal is also emitted when one of the templates used by the device +is modified or if the templates assigned to the device are changed. + +Special cases in which ``config_modified`` is not emitted +######################################################### + +This signal is not emitted when the device is created for the first time. + +It is also not emitted when templates assigned to a config object are +cleared (``post_clear`` m2m signal), this is necessary because +`sortedm2m `_, the package +we use to implement ordered templates, uses the clear action to +reorder templates (m2m relationships are first cleared and then added back), +therefore we ignore ``post_clear`` to avoid emitting signals twice +(one for the clear action and one for the add action). +Please keep this in mind if you plan on using the clear method +of the m2m manager. + +``config_status_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.config_status_changed`` + +**Arguments**: + +- ``instance``: instance of ``Config`` which got its ``status`` changed + +This signal is emitted only when the configuration status of a device has changed. + +The signal is emitted also when the m2m template relationships of a config +object are changed, but only on ``post_add`` or ``post_remove`` actions, +``post_clear`` is ignored for the same reason explained +in the previous section. + +``config_backend_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.config_backend_changed`` +**Arguments**: + +- ``instance``: instance of ``Config`` which got its ``backend`` changed +- ``old_backend``: the old backend of the config object +- ``backend``: the new backend of the config object + +It is not emitted when the device or config is created. + +``checksum_requested`` +~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.checksum_requested`` + +**Arguments**: + +- ``instance``: instance of ``Device`` for which its configuration + checksum has been requested +- ``request``: the HTTP request object + +This signal is emitted when a device requests a checksum via the controller views. + +The signal is emitted just before a successful response is returned, +it is not sent if the response was not successful. + +``config_download_requested`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.config_download_requested`` + +**Arguments**: + +- ``instance``: instance of ``Device`` for which its configuration has been + requested for download +- ``request``: the HTTP request object + +This signal is emitted when a device requests to download its configuration +via the controller views. + +The signal is emitted just before a successful response is returned, +it is not sent if the response was not successful. + +``is_working_changed`` +~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.connection.signals.is_working_changed`` + +**Arguments**: + +- ``instance``: instance of ``DeviceConnection`` +- ``is_working``: value of ``DeviceConnection.is_working`` +- ``old_is_working``: previous value of ``DeviceConnection.is_working``, + either ``None`` (for new connections), ``True`` or ``False`` +- ``failure_reason``: error message explaining reason for failure in establishing connection +- ``old_failure_reason``: previous value of ``DeviceConnection.failure_reason`` + +This signal is emitted every time ``DeviceConnection.is_working`` changes. + +It is not triggered when the device is created for the first time. + +``management_ip_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.management_ip_changed`` + +**Arguments**: + +- ``instance``: instance of ``Device`` +- ``management_ip``: value of ``Device.management_ip`` +- ``old_management_ip``: previous value of ``Device.management_ip`` + +This signal is emitted every time ``Device.management_ip`` changes. + +It is not triggered when the device is created for the first time. + +``device_registered`` +~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.device_registered`` + +**Arguments**: + +- ``instance``: instance of ``Device`` which got registered. +- ``is_new``: boolean, will be ``True`` when the device is new, + ``False`` when the device already exists + (eg: a device which gets a factory reset will register again) + +This signal is emitted when a device registers automatically through the controller +HTTP API. + +``device_name_changed`` +~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.device_name_changed`` + +**Arguments**: + +- ``instance``: instance of ``Device``. + +The signal is emitted when the device name changes. + +It is not emitted when the device is created. + +``device_group_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.device_group_changed`` + +**Arguments**: + +- ``instance``: instance of ``Device``. +- ``group_id``: primary key of ``DeviceGroup`` of ``Device`` +- ``old_group_id``: primary key of previous ``DeviceGroup`` of ``Device`` + +The signal is emitted when the device group changes. + +It is not emitted when the device is created. + +``group_templates_changed`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +**Path**: ``openwisp_controller.config.signals.group_templates_changed`` + +**Arguments**: + +- ``instance``: instance of ``DeviceGroup``. +- ``templates``: list of ``Template`` objects assigned to ``DeviceGroup`` +- ``old_templates``: list of ``Template`` objects assigned earlier to ``DeviceGroup`` + +The signal is emitted when the device group templates changes. + +It is not emitted when the device is created. + +``subnet_provisioned`` +~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.subnet_division.signals.subnet_provisioned`` + +**Arguments**: + +- ``instance``: instance of ``VpnClient``. +- ``provisioned``: dictionary of ``Subnet`` and ``IpAddress`` provisioned, + ``None`` if nothing is provisioned + +The signal is emitted when subnets and IP addresses have been provisioned +for a ``VpnClient`` for a VPN server with a subnet with +`subnet division rule <#subnet-division-app>`_. + +``vpn_server_modified`` +~~~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.vpn_server_modified`` + +**Arguments**: + +- ``instance``: instance of ``Vpn``. + +The signal is emitted when the VPN server is modified. + +``vpn_peers_changed`` +~~~~~~~~~~~~~~~~~~~~~ + +**Path**: ``openwisp_controller.config.signals.vpn_peers_changed`` + +**Arguments**: + +- ``instance``: instance of ``Vpn``. + +The signal is emitted when the peers of VPN server gets changed. + +It is only emitted for ``Vpn`` object with **WireGuard** or +**VXLAN over WireGuard** backend. diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 000000000..49fb305ab --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,59 @@ +OpenWISP Controller +=================== + +.. note:: + + This is the latest version + +OpenWISP Controller is a configuration manager that allows to automate several +networking tasks like adoption, provisioning, management VPN configuration, +X509 certificates automatic generation, revocation of x509 certificates and +a lot more 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-monitoring `_: + provides device status monitoring, collection of metrics, charts, alerts, + possibility to define custom checks +- `openwisp-firmware-upgrader `_: + automated firmware upgrades (single devices 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-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-ipam `_: + allows to manage the assignment of IP addresses used in the network +- `openwisp-notifications `_: + allows users to be aware of important events happening in the network. + +**For a more complete overview of the OpenWISP modules and architecture**, +see the +`OpenWISP Architecture Overview +`_. + + +.. toctree:: + :maxdepth: 1 + :glob: + + user/automatic-provisioning-of-subnets.rst + user/device-groups.rst + user/how-to-configure-push-updates.rst + user/how-to-setup-vxlan-over-wireguard.rst + user/how-to-setup-wireguard.rst + user/notification-alerts.rst + user/organization-limits.rst + user/send-commands.rst + user/templates-and-variables.rst + user/zerotier.rst + user/rest-api.rst + user/settings.rst + developer/developer-docs.rst diff --git a/docs/user/automatic-provisioning-of-subnets.rst b/docs/user/automatic-provisioning-of-subnets.rst new file mode 100644 index 000000000..6f09c31fa --- /dev/null +++ b/docs/user/automatic-provisioning-of-subnets.rst @@ -0,0 +1,141 @@ +How to configure automatic provisioning of subnets and IPs +---------------------------------------------------------- + +The following steps will help you configure automatic provisioning of subnets and IPs +for devices. + +1. Create a Subnet and a Subnet Division Rule +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a master subnet under which automatically generated subnets will be provisioned. + +**Note**: Choose the size of the subnet appropriately considering your use case. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png + :alt: Creating a master subnet example + +On the same page, add a **subnet division rule** that will be used to provision subnets +under the master subnet. + +The type of subnet division rule controls when subnets and IP addresses will be provisioned +for a device. The subnet division rule types currently implemented are described below. + +Device Subnet Division Rule +########################### + +This rule type is triggered whenever a device configuration (``config.Config`` model) +is created for the organization specified in the rule. + +Creating a new rule of ^Device^ type will also provision subnets and +IP addresses for existing devices of the organization automatically. + +**Note**: a device without a configuration will not trigger this rule. + +VPN Subnet Division Rule +######################## + +This rule is triggered when a VPN client template is assigned to a device, +provided the VPN server to which the VPN client template relates to has +the same subnet for which the subnet division rule is created. + +**Note:** This rule will only work for **WireGuard** and **VXLAN over WireGuard** +VPN servers. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png + :alt: Creating a subnet division rule example + +In this example, **VPN subnet division rule** is used. + +2. Create a VPN Server +~~~~~~~~~~~~~~~~~~~~~~ + +Now create a VPN Server and choose the previously created **master subnet** as the subnet for +this VPN Server. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png + :alt: Creating a VPN Server example + +3. Create a VPN Client Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a template, setting the **Type** field to **VPN Client** and **VPN** field to use the +previously created VPN Server. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png + :alt: Creating a VPN Client template example + +**Note**: You can also check the **Enable by default** field if you want to automatically +apply this template to devices that will register in future. + +4. Apply VPN Client Template to Devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With everything in place, you can now apply the VPN Client Template to devices. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png + :alt: Adding template to device example + +After saving the device, you should see all provisioned Subnets and IPs for this device +under `System Defined Variables <~system-defined-variables>`_. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png + :alt: Provisioned Subnets and IPs available as System Defined Variables example + +Voila! You can now use these variables in configuration of the device. Refer to `How to use configuration variables <~how-to-use-configuration-variables>`_ +section of this documentation to learn how to use configuration variables. + +Important notes for using Subnet Division +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- In the above example Subnet, VPN Server, and VPN Client Template belonged to the **default** organization. + You can use **Systemwide Shared** Subnet, VPN Server, or VPN Client Template too, but + Subnet Division Rule will be always related to an organization. The Subnet Division Rule will only be + triggered when such VPN Client Template will be applied to a Device having the same organization as Subnet Division Rule. + +- You can also use the configuration variables for provisioned subnets and IPs in the Template. + Each variable will be resolved differently for different devices. E.g. ``OW_subnet1_ip1`` will resolve to + ``10.0.0.1`` for one device and ``10.0.0.55`` for another. Every device gets its own set of subnets and IPs. + But don't forget to provide the default fall back values in the ^default values^ template field + (used mainly for validation). + +- The Subnet Division Rule will automatically create a reserved subnet, this subnet can be used + to provision any IP addresses that have to be created manually. The rest of the master subnet + address space **must not** be interfered with or the automation implemented in this module + will not work. + +- The above example used `VPN subnet division rule <~vpn-subnet-division-rule>`_. Similarly, + `device subnet division rule <~device-subnet-division-rule>`_ can be used, which only requires + `creating a subnet and a subnet division rule <~1-create-a-subnet-and-a-subnet-division-rule>`_. + +Limitations of Subnet Division +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the current implementation, it is not possible to change ^Size^, ^Number of Subnets^ and +^Number of IPs^ fields of an existing subnet division rule due to following reasons: + +Size +#### + +Allowing to change size of provisioned subnets of an existing subnet division rule +will require rebuilding of Subnets and IP addresses which has possibility of breaking +existing configurations. + +Number of Subnets +################# + +Allowing to decrease number of subnets of an existing subnet division +rule can create patches of unused subnets dispersed everywhere in the master subnet. +Allowing to increase number of subnets will break the continuous allocation of subnets for +every device. It can also break configuration of devices. + +Number of IPs +############# + +Allowing to decrease number of IPs of an existing subnet division rule +will lead to deletion of IP Addresses which can break configuration of devices being used. +It **is allowed** to increase number of IPs. + +If you want to make changes to any of above fields, delete the existing rule and create a +new one. The automation will provision for all existing devices that meets the criteria +for provisioning. **WARNING**: It is possible that devices get different subnets and IPs +from previous provisioning. diff --git a/docs/user/device-groups.rst b/docs/user/device-groups.rst new file mode 100644 index 000000000..0cfc2c9f9 --- /dev/null +++ b/docs/user/device-groups.rst @@ -0,0 +1,103 @@ +Device Groups +------------- + +Device Groups provide features aimed at adding specific management rules +for the devices of an organization: + +- Group similar devices by having dedicated groups for access points, routers, etc. +- Define `group metadata <~group-metadata>`_. +- Define `group configuration templates <~group-templates>`_. +- Define `group configuration variables <~group-configuration-variables>`__. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png + :alt: Device Group example + +Group Templates +~~~~~~~~~~~~~~~ + +Groups allow to define templates which are automatically assigned to devices +belonging to the group. When using this feature, keep in mind the following +important points: + +- Templates of any configuration backend can be selected, + when a device is assigned to a group, + only the templates which matches the device configuration backend are + applied to the device. +- The system will not force group templates onto devices, this means that + users can remove the applied group templates from a specific device if + needed. +- If a device group is changed, the system will automatically remove the + group templates of the old group and apply the new templates of the new + group (this operation is implemented by leveraging the + `group_templates_changed <~group_templates_changed>`_ signal). +- If the group templates are changed, the devices which belong to the group + will be automatically updated to reflect the changes + (this operation is executed in a background task). +- In case the configuration backend of a device is changed, + the system will handle this automatically too and update the group + templates accordingly (this operation is implemented by leveraging the + `config_backend_changed <~config_backend_changed>`_ signal). +- If a device does not have a configuration defined yet, but it is assigned + to a group which has templates defined, the system will automatically + create a configuration for it using the default backend specified in + `OPENWISP_CONTROLLER_DEFAULT_BACKEND <~OPENWISP_CONTROLLER_DEFAULT_BACKEND>`_ setting. + +**Note:** the list of templates shown in the edit group page do not +contain templates flagged as ^default^ or ^required^ to avoid redundancy +because those templates are automatically assigned by the system +to new devices. + +This feature works also when editing group templates or the group assigned +to a device via the `REST API <~change-device-group-detail>`__. + +Group Configuration Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Groups allow to define configuration variables which are automatically +added to the device's context in the **System Defined Variables**. +Check the `^How to use configuration variables^ section <~how-to-use-configuration-variables>`_ +to learn about precedence of different configuration variables. + +This feature works also when editing group templates or the group assigned +to a device via the `REST API <~change-device-group-detail>`__. + +Group Metadata +~~~~~~~~~~~~~~ + +Groups allow to store additional information regarding a group in the +structured metadata field (which can be accessed via the REST API). + +The metadata field allows custom structure and validation to standardize +information across all groups using the +`^OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA^ <~openwisp-controller-device-group-schema>`_ +setting. + +**Note:** *Group configuration variables* and *Group metadata* serves different purposes. +The group configuration variables should be used when the device configuration is required +to be changed for particular group of devices. Group metadata should be used to store +additional data for the devices. Group metadata is not used for configuration generation. + +Export/Import Device data +------------------------- + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png + :alt: Import / Export + +The device list page offers two buttons to export and import device data in +different formats. + +The export feature respects any filters selected in the device list. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :alt: Export + +For importing devices into the system, only the required fields are needed, +for example, the following CSV file will import a device named +``TestImport`` with mac address ``00:11:22:09:44:55`` in the organization with +UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: + + organization,name,mac_address + 3cb5e18c-0312-48ab-8dbd-038b8415bd6f,TestImport,00:11:22:09:44:55 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png + :alt: Import / Export diff --git a/docs/user/how-to-configure-push-updates.rst b/docs/user/how-to-configure-push-updates.rst new file mode 100644 index 000000000..cc6a1a978 --- /dev/null +++ b/docs/user/how-to-configure-push-updates.rst @@ -0,0 +1,92 @@ +How to configure push updates +----------------------------- + +Follow the procedure described below to enable secure SSH access from OpenWISP to your +devices, this is required to enable push updates (whenever the configuration is changed, +OpenWISP will trigger the update in the background) and/or +`firmware upgrades (via the additional module openwisp-firmware-upgrader) +`_. + +**Note**: If you have installed OpenWISP with `openwisp2 Ansbile role `_ +then you can skip the following steps. The Ansible role automatically creates a +default template to update ``authorized_keys`` on networking devices using the +default access credentials. + +1. Generate SSH key +~~~~~~~~~~~~~~~~~~~ + +First of all, we need to generate the SSH key which will be +used by OpenWISP to access the devices, to do so, you can use the following command: + +.. code-block:: shell + + echo './sshkey' | ssh-keygen -t ed25519 -C "openwisp" + +This will create two files in the current directory, one called ``sshkey`` (the private key) and one called +``sshkey.pub`` (the public key). + +Store the content of these files in a secure location. + +**Note:** Support for **ED25519** was added in OpenWrt 21.02 (requires Dropbear > 2020.79). +If you are managing devices with OpenWrt < 21, then you will need to use RSA keys: + +.. code-block:: shell + + echo './sshkey' | ssh-keygen -t rsa -b 4096 -C "openwisp" + +2. Save SSH private key in OpenWISP (access credentials) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png + :alt: add SSH private key as access credential in OpenWISP + +From the first page of OpenWISP click on "Access credentials", then click +on the **"ADD ACCESS CREDENTIALS"** button in the upper right corner +(alternatively, go to the following URL: ``/admin/connection/credentials/add/``). + +Select SSH as ``type``, enable the **Auto add** checkbox, then at the field +"Credentials type" select "SSH (private key)", now type "root" in the ``username`` field, +while in the ``key`` field you have to paste the contents of the private key just created. + +Now hit save. + +The credentials just created will be automatically enabled for all the devices in the system +(both existing devices and devices which will be added in the future). + +3. Add the public key to your devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png + :alt: Add authorized SSH public keys template to OpenWISP (OpenWRT) + +Now we need to instruct your devices to allow OpenWISP accessing via SSH, +in order to do this we need to add the contents of the public key file created in step 1 +(``sshkey.pub``) in the file ``/etc/dropbear/authorized_keys`` on the devices, the +recommended way to do this is to create a configuration template in OpenWISP: +from the first page of OpenWISP, click on "Templates", then and click on the +**"ADD TEMPLATE"** button in the upper right corner (alternatively, go to the following URL: +``/admin/config/template/add/``). + +Check **enabled by default**, then scroll down the configuration section, +click on "Configuration Menu", scroll down, click on "Files" then close the menu +by clicking again on "Configuration Menu". Now type ``/etc/dropbear/authorized_keys`` +in the ``path`` field of the file, then paste the contents of ``sshkey.pub`` in ``contents``. + +Now hit save. + +**There's a catch**: you will need to assign the template to any existing device. + +4. Test it +~~~~~~~~~~ + +Once you have performed the 3 steps above, you can test it as follows: + +1. Ensure there's at least one device turned on and connected to OpenWISP, ensure + this device has the "SSH Authorized Keys" assigned to it. +2. Ensure the celery worker of OpenWISP Controller is running (eg: ``ps aux | grep celery``) +3. SSH into the device and wait (maximum 2 minutes) until ``/etc/dropbear/authorized_keys`` + appears as specified in the template. +4. While connected via SSH to the device run the following command in the console: + ``logread -f``, now try changing the device name in OpenWISP +5. Shortly after you change the name in OpenWISP, you should see some output in the + SSH console indicating another SSH access and the configuration update being performed. diff --git a/docs/user/how-to-setup-vxlan-over-wireguard.rst b/docs/user/how-to-setup-vxlan-over-wireguard.rst new file mode 100644 index 000000000..16b09b594 --- /dev/null +++ b/docs/user/how-to-setup-vxlan-over-wireguard.rst @@ -0,0 +1,102 @@ +How to setup VXLAN over WireGuard tunnels +----------------------------------------- + +By following these steps, you will be able to setup layer 2 VXLAN tunnels +encapsulated in WireGuard tunnels which work on layer 3. + +**Note:** This example uses **Shared systemwide (no organization)** option as +the organization for VPN server and VPN client template. You can use any +organization as long as VPN server, VPN client template and Device has same +organization. + +1. Create VPN server configuration for VXLAN over WireGuard +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. +2. We will set **Name** of this VPN server ``Wireguard VXLAN`` and **Host** as + ``wireguard-vxlan-server.mydomain.com`` (update this to point to your + WireGuard VXLAN VPN server). +3. Select ``VXLAN over WireGuard`` from the dropdown as **VPN Backend**. +4. When using VXLAN over WireGuard, OpenWISP takes care of managing IP addresses + (assigning an IP address to each VPN peer). You can create a new subnet or + select an existing one from the dropdown menu. You can also assign an + **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to + configure. This IP address will be used by the WireGuard interface on + server. +5. We have set the **Webhook Endpoint** as ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` + for this example. You will need to update this according to you VPN upgrader + endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be + used to ensure that configuration upgrades are requested from trusted + sources. + + **Note**: If you are following this tutorial for also setting up WireGuard + VPN server, just substitute ``wireguard-server.mydomain.com`` with hostname + of your VPN server and follow the steps in next section. + +6. Under the configuration section, set the name of WireGuard tunnel 1 interface. + We have used ``wg0`` in this example. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png + :alt: WireGuard VPN VXLAN server configuration example 1 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png + :alt: WireGuard VPN VXLAN server configuration example 2 + +7. After clicking on **Save and continue editing**, you will see that OpenWISP + has automatically created public and private key for WireGuard server in + **System Defined Variables** along with internal IP address information. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png + :alt: WireGuard VXLAN VPN server configuration example 3 + +2. Deploy Wireguard VXLAN VPN Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you haven't already setup WireGuard on your VPN server, this will be a good +time do so. We recommend using the `ansible-wireguard-openwisp `_ +role for installing WireGuard since it also installs scripts that allows +OpenWISP to manage WireGuard VPN server along with VXLAN tunnels. + +Pay attention to the VPN server attributes used in your playbook. It should be same as +VPN server configuration in OpenWISP. + +3. Create VPN client template for WireGuard VXLAN VPN Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Visit ``/admin/config/template/add/`` to add a new template. +2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and + select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can + be applied to. For this example, we will leave it to ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here + it is ``Wireguard VXLAN``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will make + OpenWISP to automatically generate public and private keys and provision IP + address for each WireGuard VPN client along with VXLAN Network Indentifier(VNI). +6. After clicking on **Save and continue editing** button, you will see details + of *Wireguard VXLAN* VPN server in **System Defined Variables**. The template + configuration will be automatically generated which you can tweak + accordingly. We will use the automatically generated VPN client configuration + for this example. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png + :alt: WireGuard VXLAN VPN client template example + +4. Apply Wireguard VXLAN VPN template to devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Note**: This step assumes that you already have a device registered on +OpenWISP. Register or create a device before proceeding. + +1. Open the **Configuration** tab of the concerned device. +2. Select the *WireGuard VXLAN Client* template. +3. Upon clicking on **Save and continue editing** button, you will see some + entries in **System Defined Variables**. It will contain internal IP address, + private and public key for the WireGuard client on the device and details of + WireGuard VPN server along with VXLAN Network Identifier(VNI) of this device. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png + :alt: WireGuard VXLAN VPN device configuration example + +**Voila!** You have successfully configured OpenWISP to manage VXLAN over +WireGuard tunnels for your devices. diff --git a/docs/user/how-to-setup-wireguard.rst b/docs/user/how-to-setup-wireguard.rst new file mode 100644 index 000000000..06d09ca65 --- /dev/null +++ b/docs/user/how-to-setup-wireguard.rst @@ -0,0 +1,101 @@ +How to setup WireGuard tunnels +------------------------------ + +Follow the procedure described below to setup WireGuard tunnels on your devices. + +**Note:** This example uses **Shared systemwide (no organization)** option as +the organization for VPN server and VPN client template. You can use any +organization as long as VPN server, VPN client template and Device has same +organization. + +1. Create VPN server configuration for WireGuard +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. +2. We will set **Name** of this VPN server ``Wireguard`` and **Host** as + ``wireguard-server.mydomain.com`` (update this to point to your + WireGuard VPN server). +3. Select ``WireGuard`` from the dropdown as **VPN Backend**. +4. When using WireGuard, OpenWISP takes care of managing IP addresses + (assigning an IP address to each VPN peer). You can create a new subnet or + select an existing one from the dropdown menu. You can also assign an + **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to + configure. This IP address will be used by the WireGuard interface on + server. +5. We have set the **Webhook Endpoint** as ``https://wireguard-server.mydomain.com:8081/trigger-update`` + for this example. You will need to update this according to you VPN upgrader + endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be + used to ensure that configuration upgrades are requested from trusted + sources. + + **Note**: If you are following this tutorial for also setting up WireGuard + VPN server, just substitute ``wireguard-server.mydomain.com`` with hostname + of your VPN server and follow the steps in next section. + +6. Under the configuration section, set the name of WireGuard tunnel 1 interface. + We have used ``wg0`` in this example. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png + :alt: WireGuard VPN server configuration example 1 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png + :alt: WireGuard VPN server configuration example 2 + +7. After clicking on **Save and continue editing**, you will see that OpenWISP + has automatically created public and private key for WireGuard server in + **System Defined Variables** along with internal IP address information. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png + :alt: WireGuard VPN server configuration example 3 + +2. Deploy Wireguard VPN Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you haven't already setup WireGuard on your VPN server, this will be a good +time do so. We recommend using the `ansible-wireguard-openwisp `_ +role for installing WireGuard since it also installs scripts that allows +OpenWISP to manage WireGuard VPN server. + +Pay attention to the VPN server attributes used in your playbook. It should be same as +VPN server configuration in OpenWISP. + +3. Create VPN client template for WireGuard VPN Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Visit ``/admin/config/template/add/`` to add a new template. +2. Set ``Wireguard Client`` as **Name** (you can set whatever you want) and + select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can + be applied to. For this example, we will leave it to ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here + it is ``Wireguard``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will make + OpenWISP to automatically generate public and private keys and provision IP + address for each WireGuard VPN client. +6. After clicking on **Save and continue editing** button, you will see details + of *Wireguard* VPN server in **System Defined Variables**. The template + configuration will be automatically generated which you can tweak + accordingly. We will use the automatically generated VPN client configuration + for this example. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png + :alt: WireGuard VPN client template example + +4. Apply Wireguard VPN template to devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Note**: This step assumes that you already have a device registered on +OpenWISP. Register or create a device before proceeding. + +1. Open the **Configuration** tab of the concerned device. +2. Select the *WireGuard Client* template. +3. Upon clicking on **Save and continue editing** button, you will see some + entries in **System Defined Variables**. It will contain internal IP address, + private and public key for the WireGuard client on the device along with + details of WireGuard VPN server. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png + :alt: WireGuard VPN device configuration example + +**Voila!** You have successfully configured OpenWISP to manage WireGuard +tunnels for your devices. diff --git a/docs/user/notification-alerts.rst b/docs/user/notification-alerts.rst new file mode 100644 index 000000000..3879a09d8 --- /dev/null +++ b/docs/user/notification-alerts.rst @@ -0,0 +1,10 @@ +Default Alerts / Notifications +------------------------------ + ++-----------------------+---------------------------------------------------------------------+ +| Notification Type | Use | ++-----------------------+---------------------------------------------------------------------+ +| ``config_error`` | Fires when status of a device configuration changes to ``error``. | ++-----------------------+---------------------------------------------------------------------+ +| ``device_registered`` | Fires when a new device is registered automatically on the network. | ++-----------------------+---------------------------------------------------------------------+ diff --git a/docs/user/organization-limits.rst b/docs/user/organization-limits.rst new file mode 100644 index 000000000..2adbbadcf --- /dev/null +++ b/docs/user/organization-limits.rst @@ -0,0 +1,11 @@ +Organization Limits +------------------- + +Allows configuring following limits for each organization: + +- Limit number of devices managed by the organization. + +You can change the limits from the organization's admin page: + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png + :alt: Organization limits diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst new file mode 100644 index 000000000..f3592b54f --- /dev/null +++ b/docs/user/rest-api.rst @@ -0,0 +1,1130 @@ +REST API Reference +------------------ + +Live documentation +~~~~~~~~~~~~~~~~~~ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.png + +A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. + +Browsable web interface +~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.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. + +Authentication +~~~~~~~~~~~~~~ + +See openwisp-users: `authenticating with the user token +`_. + +When browsing the API via the `Live documentation <#live-documentation>`_ +or the `Browsable web page <#browsable-web-interface>`_, you can also use +the session authentication by logging in the django admin. + +Pagination +~~~~~~~~~~ + +All *list* endpoints support the ``page_size`` parameter that allows paginating +the results in conjunction with the ``page`` parameter. + +.. code-block:: text + + GET /api/v1/controller/template/?page_size=10 + GET /api/v1/controller/template/?page_size=10&page=2 + +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. + +List devices +############ + +.. code-block:: text + + GET /api/v1/controller/device/ + +**Available filters** + +You can filter a list of devices based on their configuration +status using the ``status`` (e.g modified, applied, or error). + +.. code-block:: text + + GET /api/v1/controller/device/?config__status={status} + +You can filter a list of devices based on their configuration backend +using the ``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). + +.. code-block:: text + + GET /api/v1/controller/device/?config__backend={backend} + +You can filter a list of devices based on their +organization using the ``organization_id`` or ``organization_slug``. + +.. code-block:: text + + GET /api/v1/controller/device/?organization={organization_id} + +.. code-block:: text + + GET /api/v1/controller/device/?organization_slug={organization_slug} + +You can filter a list of devices based on their +configuration templates using the ``template_id``. + +.. code-block:: text + + GET /api/v1/controller/device/?config__templates={template_id} + +You can filter a list of devices based on +their device group using the ``group_id``. + +.. code-block:: text + + GET /api/v1/controller/device/?group={group_id} + +You can filter a list of devices that have a device +location object using the ``with_geo`` (eg. true or false). + +.. code-block:: text + + GET /api/v1/controller/device/?with_geo={with_geo} + +You can filter a list of devices based on +their creation time using the ``creation_time``. + +.. code-block:: text + + # Created exact + GET /api/v1/controller/device/?created={creation_time} + + # Created greater than or equal to + GET /api/v1/controller/device/?created__gte={creation_time} + + # Created is less than + GET /api/v1/controller/device/?created__lt={creation_time} + + + +Create device +############# + +.. code-block:: text + + POST /api/v1/controller/device/ + +Get device detail +################# + +.. code-block:: text + + GET /api/v1/controller/device/{id}/ + +Download device configuration +############################# + +.. code-block:: text + + GET /api/v1/controller/device/{id}/configuration/ + +The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific device. + +Change details of device +######################## + +.. code-block:: text + + PUT /api/v1/controller/device/{id}/ + +Patch details of device +####################### + +.. code-block:: text + + PATCH /api/v1/controller/device/{id}/ + +**Note**: To assign, unassign, and change the order of the assigned templates add, +remove, and change the order of the ``{id}`` of the templates under the ``config`` field in the JSON response respectively. +Moreover, you can also select and unselect templates in the HTML Form of the Browsable API. + +The required template(s) from the organization(s) of the device will added automatically +to the ``config`` and cannot be removed. + +**Example usage**: For assigning template(s) add the/their {id} to the config of a device, + +.. code-block:: shell + + curl -X PATCH \ + http://127.0.0.1:8000/api/v1/controller/device/76b7d9cc-4ffd-4a43-b1b0-8f8befd1a7c0/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: application/json' \ + -d '{ + "config": { + "templates": ["4791fa4c-2cef-4f42-8bb4-c86018d71bd3"] + } + }' + +**Example usage**: For removing assigned templates, simply remove the/their {id} from the config of a device, + +.. code-block:: shell + + curl -X PATCH \ + http://127.0.0.1:8000/api/v1/controller/device/76b7d9cc-4ffd-4a43-b1b0-8f8befd1a7c0/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: application/json' \ + -d '{ + "config": { + "templates": [] + } + }' + +**Example usage**: For reordering the templates simply change their order from the config of a device, + +.. code-block:: shell + + curl -X PATCH \ + http://127.0.0.1:8000/api/v1/controller/device/76b7d9cc-4ffd-4a43-b1b0-8f8befd1a7c0/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -H 'postman-token: b3f6a1cc-ff13-5eba-e460-8f394e485801' \ + -d '{ + "config": { + "templates": [ + "c5bbc697-170e-44bc-8eb7-b944b55ee88f", + "4791fa4c-2cef-4f42-8bb4-c86018d71bd3" + ] + } + }' + +Delete device +############# + +.. code-block:: text + + DELETE /api/v1/controller/device/{id}/ + +List device connections +####################### + +.. code-block:: text + + GET /api/v1/controller/device/{id}/connection/ + +Create device connection +######################## + +.. code-block:: text + + POST /api/v1/controller/device/{id}/connection/ + +Get device connection detail +############################ + +.. code-block:: text + + GET /api/v1/controller/device/{id}/connection/{id}/ + +Change device connection detail +############################### + +.. code-block:: text + + PUT /api/v1/controller/device/{id}/connection/{id}/ + +Patch device connection detail +############################## + +.. code-block:: text + + PATCH /api/v1/controller/device/{id}/connection/{id}/ + +Delete device connection +######################## + +.. code-block:: text + + DELETE /api/v1/controller/device/{id}/connection/{id}/ + +List credentials +################ + +.. code-block:: text + + GET /api/v1/connection/credential/ + +Create credential +################# + +.. code-block:: text + + POST /api/v1/connection/credential/ + +Get credential detail +##################### + +.. code-block:: text + + GET /api/v1/connection/credential/{id}/ + +Change credential detail +######################## + +.. code-block:: text + + PUT /api/v1/connection/credential/{id}/ + +Patch credential detail +####################### + +.. code-block:: text + + PATCH /api/v1/connection/credential/{id}/ + +Delete credential +################# + +.. code-block:: text + + DELETE /api/v1/connection/credential/{id}/ + +List commands of a device +######################### + +.. code-block:: text + + GET /api/v1/controller/device/{id}/command/ + +Execute a command a device +########################## + +.. code-block:: text + + POST /api/v1/controller/device/{id}/command/ + +Get command details +################### + +.. code-block:: text + + GET /api/v1/controller/device/{device_id}/command/{command_id}/ + +List device groups +################## + +.. code-block:: text + + GET /api/v1/controller/group/ + +**Available filters** + +You can filter a list of device groups based on their +organization using the ``organization_id`` or ``organization_slug``. + +.. code-block:: text + + GET /api/v1/controller/group/?organization={organization_id} + +.. code-block:: text + + GET /api/v1/controller/group/?organization_slug={organization_slug} + +You can filter a list of device groups that have a +device object using the ``empty`` (eg. true or false). + +.. code-block:: text + + GET /api/v1/controller/group/?empty={empty} + + +Create device group +################### + +.. code-block:: text + + POST /api/v1/controller/group/ + +Get device group detail +####################### + +.. code-block:: text + + GET /api/v1/controller/group/{id}/ + +Change device group detail +########################## + +.. code-block:: text + + PUT /api/v1/controller/group/{id}/ + +This endpoint allows to change the `group templates <#group-templates>`_ too. + +Get device group from certificate common name +############################################# + +.. code-block:: text + + GET /api/v1/controller/cert/{common_name}/group/ + +This endpoint can be used to retrieve group information and metadata by the +common name of a certificate used in a VPN client tunnel, this endpoint is +used in layer 2 tunneling solutions for firewall/captive portals. + +It is also possible to filter device group by providing organization slug +of certificate's organization as show in the example below: + +.. code-block:: text + + GET /api/v1/controller/cert/{common_name}/group/?org={org1_slug},{org2_slug} + +Get device location +################### + +.. code-block:: text + + + GET /api/v1/controller/device/{id}/location/ + + +Create device location +###################### + +.. code-block:: text + + PUT /api/v1/controller/device/{id}/location/ + +You can create ``DeviceLocation`` object by using primary +keys of existing ``Location`` and ``FloorPlan`` objects as shown in +the example below. + +.. code-block:: json + + { + "location": "f0cb5762-3711-4791-95b6-c2f6656249fa", + "floorplan": "dfeb6724-aab4-4533-aeab-f7feb6648acd", + "indoor": "-36,264" + } + +**Note:** The ``indoor`` field represents the coordinates of the +point placed on the image from the top left corner. E.g. if you +placed the pointer on the top left corner of the floorplan image, +its indoor coordinates will be ``0,0``. + +.. code-block:: text + + curl -X PUT \ + http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: application/json' \ + -d '{ + "location": "f0cb5762-3711-4791-95b6-c2f6656249fa", + "floorplan": "dfeb6724-aab4-4533-aeab-f7feb6648acd", + "indoor": "-36,264" + }' + +You can also create related ``Location`` and ``FloorPlan`` objects for the +device directly from this endpoint. + +The following example demonstrates creating related location +object in a single request. + +.. code-block:: json + + { + "location": { + "name": "Via del Corso", + "address": "Via del Corso, Roma, Italia", + "geometry": { + "type": "Point", + "coordinates": [12.512124, 41.898903] + }, + "type": "outdoor", + } + } + +.. code-block:: text + + curl -X PUT \ + http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: application/json' \ + -d '{ + "location": { + "name": "Via del Corso", + "address": "Via del Corso, Roma, Italia", + "geometry": { + "type": "Point", + "coordinates": [12.512124, 41.898903] + }, + "type": "outdoor" + } + }' + +**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** +format, like following: + +.. code-block:: json + + { + "location": { + "name": "Via del Corso", + "address": "Via del Corso, Roma, Italia", + "geometry": "POINT (12.512124 41.898903)", + "type": "outdoor", + } + } + +Similarly, you can create ``Floorplan`` object with the same request. +But, note that a ``FloorPlan`` can be added to ``DeviceLocation`` only +if the related ``Location`` object defines an indoor location. The example +below demonstrates creating both ``Location`` and ``FloorPlan`` objects. + +.. code-block:: text + + // This is not a valid JSON object. The JSON format is + // only used for showing available fields. + { + "location.name": "Via del Corso", + "location.address": "Via del Corso, Roma, Italia", + "location.geometry.type": "Point", + "location.geometry.coordinates": [12.512124, 41.898903] + "location.type": "outdoor", + "floorplan.floor": 1, + "floorplan.image": floorplan.png, + } + +.. code-block:: text + + curl -X PUT \ + http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ + -F 'location.name=Via del Corso' \ + -F 'location.address=Via del Corso, Roma, Italia' \ + -F location.geometry.type=Point \ + -F 'location.geometry.coordinates=[12.512124, 41.898903]' \ + -F location.type=indoor \ + -F floorplan.floor=1 \ + -F 'floorplan.image=@floorplan.png' + +**Note:** The request in above example uses ``multipart content-type`` +for uploading floorplan image. + +You can also use an existing ``Location`` object and create a new +floorplan for that location using this endpoint. + +.. code-block:: text + + // This is not a valid JSON object. The JSON format is + // only used for showing available fields. + { + "location": "f0cb5762-3711-4791-95b6-c2f6656249fa", + "floorplan.floor": 1, + "floorplan.image": floorplan.png + } + +.. code-block:: text + + curl -X PUT \ + http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/location/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ + -F location=f0cb5762-3711-4791-95b6-c2f6656249fa \ + -F floorplan.floor=1 \ + -F 'floorplan.image=@floorplan.png' + +Change details of device location +################################# + +.. code-block:: text + + PUT /api/v1/controller/device/{id}/location/ + +**Note:** This endpoint can be used to update related ``Location`` +and ``Floorplan`` objects. Refer `examples of "Create device location" +section for information on payload format <#create-device-location>`_. + +Delete device location +###################### + +.. code-block:: text + + DELETE /api/v1/controller/device/{id}/location/ + +Get device coordinates +###################### + +.. code-block:: text + + GET /api/v1/controller/device/{id}/coordinates/ + +**Note:** This endpoint is intended to be used by devices. + +This endpoint skips multi-tenancy and permission checks if the +device ``key`` is passed as ``query_param`` because the system +assumes that the device is updating it's position. + +.. code-block:: text + + curl -X GET \ + 'http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/coordinates/?key=10a0cb5a553c71099c0e4ef236435496' + +Update device coordinates +######################### + +.. code-block:: text + + PUT /api/v1/controller/device/{id}/coordinates/ + +**Note:** This endpoint is intended to be used by devices. + +This endpoint skips multi-tenancy and permission checks if the +device ``key`` is passed as ``query_param`` because the system +assumes that the device is updating it's position. + +.. code-block:: json + + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [12.512124, 41.898903] + }, + } + +.. code-block:: text + + curl -X PUT \ + 'http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/coordinates/?key=10a0cb5a553c71099c0e4ef236435496' \ + -H 'content-type: application/json' \ + -d '{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [12.512124, 41.898903] + }, + }' + +List locations +############## + +.. code-block:: text + + GET /api/v1/controller/location/ + +**Available filters** + +You can filter using ``organization_id`` or ``organization_slug`` +to get list locations that belongs to an organization. + +.. code-block:: text + + GET /api/v1/controller/location/?organization={organization_id} + +.. code-block:: text + + GET /api/v1/controller/location/?organization_slug={organization_slug} + +Create location +############### + +.. code-block:: text + + POST /api/v1/controller/location/ + +If you are creating an ``indoor`` location, you can use this endpoint +to create floorplan for the location. + +The following example demonstrates creating floorplan along with location +in a single request. + +.. code-block:: text + + { + "name": "Via del Corso", + "address": "Via del Corso, Roma, Italia", + "geometry.type": "Point", + "geometry.location": [12.512124, 41.898903], + "type": "indoor", + "is_mobile": "false", + "floorplan.floor": "1", + "floorplan.image": floorplan.png, + "organization": "1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05" + } + +.. code-block:: text + + curl -X POST \ + http://127.0.0.1:8000/api/v1/controller/location/ \ + -H 'authorization: Bearer dc8d497838d4914c9db9aad9b6ec66f6c36ff46b' \ + -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ + -F 'name=Via del Corso' \ + -F 'address=Via del Corso, Roma, Italia' \ + -F geometry.type=Point \ + -F 'geometry.coordinates=[12.512124, 41.898903]' \ + -F type=indoor \ + -F is_mobile=false \ + -F floorplan.floor=1 \ + -F 'floorplan.image=@floorplan.png' \ + -F organization=1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05 + +**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** +format, like following: + +.. code-block:: text + + { + "name": "Via del Corso", + "address": "Via del Corso, Roma, Italia", + "geometry": "POINT (12.512124 41.898903)", + "type": "indoor", + "is_mobile": "false", + "floorplan.floor": "1", + "floorplan.image": floorplan.png, + "organization": "1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05" + } + +Get location details +#################### + +.. code-block:: text + + GET /api/v1/controller/location/{pk}/ + +Change location details +####################### + +.. code-block:: text + + PUT /api/v1/controller/location/{pk}/ + +**Note**: Only the first floorplan data present can be +edited or changed. Setting the ``type`` of location to +outdoor will remove all the floorplans associated with it. + +Refer `examples of "Create location" +section for information on payload format <#create-location>`_. + +Delete location +############### + +.. code-block:: text + + DELETE /api/v1/controller/location/{pk}/ + +List devices in a location +########################## + +.. code-block:: text + + GET /api/v1/controller/location/{id}/device/ + +List locations with devices deployed (in GeoJSON format) +######################################################## + +**Note**: this endpoint will only list locations that have been assigned to a device. + +.. code-block:: text + + GET /api/v1/controller/location/geojson/ + +**Available filters** + +You can filter using ``organization_id`` or ``organization_slug`` +to get list location of devices from that organization. + +.. code-block:: text + + GET /api/v1/controller/location/geojson/?organization_id={organization_id} + +.. code-block:: text + + GET /api/v1/controller/location/geojson/?organization_slug={organization_slug} + +List floorplans +############### + +.. code-block:: text + + GET /api/v1/controller/floorplan/ + +**Available filters** + +You can filter using ``organization_id`` or ``organization_slug`` +to get list floorplans that belongs to an organization. + +.. code-block:: text + + GET /api/v1/controller/floorplan/?organization={organization_id} + +.. code-block:: text + + GET /api/v1/controller/floorplan/?organization_slug={organization_slug} + +Create floorplan +################ + +.. code-block:: text + + POST /api/v1/controller/floorplan/ + +Get floorplan details +##################### + +.. code-block:: text + + GET /api/v1/controller/floorplan/{pk}/ + +Change floorplan details +######################## + +.. code-block:: text + + PUT /api/v1/controller/floorplan/{pk}/ + +Delete floorplan +################ + +.. code-block:: text + + DELETE /api/v1/controller/floorplan/{pk}/ + +List templates +############## + +.. code-block:: text + + GET /api/v1/controller/template/ + +**Available filters** + +You can filter a list of templates based on their organization +using the ``organization_id`` or ``organization_slug``. + +.. code-block:: text + + GET /api/v1/controller/template/?organization={organization_id} + +.. code-block:: text + + GET /api/v1/controller/template/?organization_slug={organization_slug} + +You can filter a list of templates based on their backend using +the ``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). + +.. code-block:: text + + GET /api/v1/controller/template/?backend={backend} + +You can filter a list of templates based on their +type using the ``type`` (eg. vpn or generic). + +.. code-block:: text + + GET /api/v1/controller/template/?type={type} + +You can filter a list of templates that are enabled +by default or not using the ``default`` (eg. true or false). + +.. code-block:: text + + GET /api/v1/controller/template/?default={default} + +You can filter a list of templates that are required +or not using the ``required`` (eg. true or false). + +.. code-block:: text + + GET /api/v1/controller/template/?required={required} + +You can filter a list of templates based on +their creation time using the ``creation_time``. + +.. code-block:: text + + # Created exact + + GET /api/v1/controller/template/?created={creation_time} + + # Created greater than or equal to + + GET /api/v1/controller/template/?created__gte={creation_time} + + # Created is less than + + GET /api/v1/controller/template/?created__lt={creation_time} + +Create template +############### + +.. code-block:: text + + POST /api/v1/controller/template/ + +Get template detail +################### + +.. code-block:: text + + GET /api/v1/controller/template/{id}/ + +Download template configuration +############################### + +.. code-block:: text + + GET /api/v1/controller/template/{id}/configuration/ + +The above endpoint triggers the download of a ``tar.gz`` file +containing the generated configuration for that specific template. + +Change details of template +########################## + +.. code-block:: text + + PUT /api/v1/controller/template/{id}/ + +Patch details of template +######################### + +.. code-block:: text + + PATCH /api/v1/controller/template/{id}/ + +Delete template +############### + +.. code-block:: text + + DELETE /api/v1/controller/template/{id}/ + +List VPNs +######### + +.. code-block:: text + + GET /api/v1/controller/vpn/ + +**Available filters** + +You can filter a list of vpns based +on their backend using the ``backend`` +(e.g openwisp_controller.vpn_backends.OpenVpn +or openwisp_controller.vpn_backends.Wireguard). + +.. code-block:: text + + GET /api/v1/controller/vpn/?backend={backend} + +You can filter a list of vpns based on their subnet using the ``subnet_id``. + +.. code-block:: text + + GET /api/v1/controller/vpn/?subnet={subnet_id} + +You can filter a list of vpns based on their organization +using the ``organization_id`` or ``organization_slug``. + +.. code-block:: text + + GET /api/v1/controller/vpn/?organization={organization_id} + +.. code-block:: text + + GET /api/v1/controller/vpn/?organization_slug={organization_slug} + +Create VPN +########## + +.. code-block:: text + + POST /api/v1/controller/vpn/ + +Get VPN detail +############## + +.. code-block:: text + + GET /api/v1/controller/vpn/{id}/ + +Download VPN configuration +########################## + +.. code-block:: text + + GET /api/v1/controller/vpn/{id}/configuration/ + +The above endpoint triggers the download of a ``tar.gz`` file +containing the generated configuration for that specific VPN. + +Change details of VPN +##################### + +.. code-block:: text + + PUT /api/v1/controller/vpn/{id}/ + +Patch details of VPN +#################### + +.. code-block:: text + + PATCH /api/v1/controller/vpn/{id}/ + +Delete VPN +########## + +.. code-block:: text + + DELETE /api/v1/controller/vpn/{id}/ + +List CA +####### + +.. code-block:: text + + GET /api/v1/controller/ca/ + +Create new CA +############# + +.. code-block:: text + + POST /api/v1/controller/ca/ + +Import existing CA +################## + +.. code-block:: text + + POST /api/v1/controller/ca/ + +**Note**: To import an existing CA, only ``name``, ``certificate`` +and ``private_key`` fields have to be filled in the ``HTML`` form or +included in the ``JSON`` format. + +Get CA Detail +############# + +.. code-block:: text + + GET /api/v1/controller/ca/{id}/ + +Change details of CA +#################### + +.. code-block:: text + + PUT /api/v1/controller/ca/{id}/ + +Patch details of CA +################### + +.. code-block:: text + + PATCH /api/v1/controller/ca/{id}/ + +Download CA(crl) +################ + +.. code-block:: text + + GET /api/v1/controller/ca/{id}/crl/ + +The above endpoint triggers the download of ``{id}.crl`` file containing +up to date CRL of that specific CA. + +Delete CA +######### + +.. code-block:: text + + DELETE /api/v1/controller/ca/{id}/ + +Renew CA +######## + +.. code-block:: text + + POST /api/v1/controller/ca/{id}/renew/ + +List Cert +######### + +.. code-block:: text + + GET /api/v1/controller/cert/ + +Create new Cert +############### + +.. code-block:: text + + POST /api/v1/controller/cert/ + +Import existing Cert +#################### + +.. code-block:: text + + POST /api/v1/controller/cert/ + +**Note**: To import an existing Cert, only ``name``, ``ca``, +``certificate`` and ``private_key`` fields have to be filled +in the ``HTML`` form or included in the ``JSON`` format. + +Get Cert Detail +############### + +.. code-block:: text + + GET /api/v1/controller/cert/{id}/ + +Change details of Cert +###################### + +.. code-block:: text + + PUT /api/v1/controller/cert/{id}/ + +Patch details of Cert +##################### + +.. code-block:: text + + PATCH /api/v1/controller/cert/{id}/ + +Delete Cert +########### + +.. code-block:: text + + DELETE /api/v1/controller/cert/{id}/ + +Renew Cert +########## + +.. code-block:: text + + POST /api/v1/controller/cert/{id}/renew/ + +Revoke Cert +########### + +.. code-block:: text + + POST /api/v1/controller/cert/{id}/revoke/ diff --git a/docs/user/send-commands.rst b/docs/user/send-commands.rst new file mode 100644 index 000000000..a96b87f82 --- /dev/null +++ b/docs/user/send-commands.rst @@ -0,0 +1,192 @@ +Sending Commands to Devices +--------------------------- + +By default, there are three options in the **Send Command** dropdown: + +1. Reboot +2. Change Password +3. Custom Command + +While the first two options are self-explanatory, the **custom command** option +allows you to execute any command on the device as shown in the example below. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/commands_demo.gif + :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/commands_demo.gif + :alt: Executing commands on device example + +**Note**: in order for this feature to work, a device needs to have at least +one **Access Credential** (see `How to configure push updates <~how-to-configure-push-updates>`__). + +The **Send Command** button will be hidden until the device +has at least one **Access Credential**. + +If you need to allow your users to quickly send specific commands that are used often in your +network regardless of your users' knowledge of Linux shell commands, you can add new commands +by following instructions in the `^How to define new options in the commands menu^ +<~how-to-define-new-options-in-the-commands-menu>`_ section below. + +If you are an advanced user and want to register commands programatically, then refer to +`^Register / Unregistering commands^ <~registering--unregistering-commands>`_ section. + +How to define new options in the commands menu +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's explore to define new custom commands +to help users perform additional management actions +without having to be Linux/Unix experts. + +We can do so by using the ``OPENWISP_CONTROLLER_USER_COMMANDS`` django setting. + +The following example defines a simple command that can ``ping`` an input +``destination_address`` through a network interface, ``interface_name``. + +.. code-block:: python + + ~ In yourproject/settings.py + + def ping_command_callable(destination_address, interface_name=None): + command = f'ping -c 4 {destination_address}' + if interface_name: + command += f' -I {interface_name}' + return command + + OPENWISP_CONTROLLER_USER_COMMANDS = [ + ( + 'ping', + { + 'label': 'Ping', + 'schema': { + 'title': 'Ping', + 'type': 'object', + 'required': ['destination_address'], + 'properties': { + 'destination_address': { + 'type': 'string', + 'title': 'Destination Address', + }, + 'interface_name': { + 'type': 'string', + 'title': 'Interface Name', + }, + }, + 'message': 'Destination Address cannot be empty', + 'additionalProperties': False, + }, + 'callable': ping_command_callable, + } + ) + ] + +The above code will add the ^Ping^ command in the user interface as show +in the GIF below: + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/ping_command_example.gif + :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/ping_command_example.gif + :alt: Adding a ^ping^ command + +The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of ``tuple`` +each containing two elements. The first element of the tuple should contain an +identifier for the command and the second element should contain a ``dict`` +defining configuration of the command. + +Command Configuration +##################### + +The ``dict`` defining configuration for command should contain following keys: + +1. ``label`` +^^^^^^^^^^^^ + +A ``str`` defining label for the command used internally by Django. + +2. ``schema`` +^^^^^^^^^^^^^ + +A ``dict`` defining `JSONSchema `_ for inputs of command. +You can specify the inputs for your command, add rules for performing validation +and make inputs required or optional. + +Here is a detailed explanation of the schema used in above example: + +.. code-block:: python + + { + ~ Name of the command displayed in ^Send Command^ widget + 'title': 'Ping', + ~ Use type ^object^ if the command needs to accept inputs + ~ Use type ^null^ if the command does not accepts any input + 'type': 'object', + ~ Specify list of inputs that are required + 'required': ['destination_address'], + ~ Define the inputs for the commands along with their properties + 'properties': { + 'destination_address': { + ~ type of the input value + 'type': 'string', + ~ label used for displaying this input field + 'title': 'Destination Address', + }, + 'interface_name': { + 'type': 'string', + 'title': 'Interface Name', + }, + }, + ~ Error message to be shown if validation fails + 'message': 'Destination Address cannot be empty'), + ~ Whether specifying addtionaly inputs is allowed from the input form + 'additionalProperties': False, + } + +This example uses only handful of properties available in JSONSchema. You can +experiment with other properties of JSONSchema for schema of your command. + +3. ``callable`` +^^^^^^^^^^^^^^^ + +A ``callable`` or ``str`` defining dotted path to a callable. It should return +the command (``str``) to be executed on the device. Inputs of the command are +passed as arguments to this callable. + +The example above includes a callable(``ping_command_callable``) for +``ping`` command. + +Registering / Unregistering Commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OpenWISP Controller provides registering and unregistering commands +through utility functions ``openwisp_controller.connection.commands.register_command`` +and ``openwisp_notifications.types.unregister_notification_type``. +You can use these functions to register or unregister commands +from your code. + +**Note**: These functions are to be used as an alternative to the +`^OPENWISP_CONTROLLER_USER_COMMANDS^ <~openwisp-controller-user-commands>`_ +when `developing custom modules based on openwisp-controller +<~extending-openwisp-controller>`_ or when developing custom third party +apps. + +``register_command`` +#################### + ++--------------------+------------------------------------------------------------------+ +| Parameter | Description | ++--------------------+------------------------------------------------------------------+ +| ``command_name`` | A ``str`` defining identifier for the command. | ++--------------------+------------------------------------------------------------------+ +| ``command_config`` | A ``dict`` defining configuration of the command | +| | as shown in `^Command Configuration^ <~command-configuration>`_. | ++--------------------+------------------------------------------------------------------+ + +**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already +registered with the same name. + +``unregister_command`` +###################### + ++--------------------+-----------------------------------------+ +| Parameter | Description | ++--------------------+-----------------------------------------+ +| ``command_name`` | A ``str`` defining name of the command. | ++--------------------+-----------------------------------------+ + +**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not exists. diff --git a/docs/user/settings.rst b/docs/user/settings.rst new file mode 100644 index 000000000..05e869df4 --- /dev/null +++ b/docs/user/settings.rst @@ -0,0 +1,698 @@ +Settings +-------- + +You can change the values for the following variables in +``settings.py`` to configure your instance of openwisp-controller. + +``OPENWISP_SSH_AUTH_TIMEOUT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``int`` | ++--------------+-------------+ +| **default**: | ``2`` | ++--------------+-------------+ +| **unit**: | ``seconds`` | ++--------------+-------------+ + +Configure timeout to wait for an authentication response when establishing a SSH connection. + +``OPENWISP_SSH_BANNER_TIMEOUT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``int`` | ++--------------+-------------+ +| **default**: | ``60`` | ++--------------+-------------+ +| **unit**: | ``seconds`` | ++--------------+-------------+ + +Configure timeout to wait for the banner to be presented when establishing a SSH connection. + +``OPENWISP_SSH_COMMAND_TIMEOUT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``int`` | ++--------------+-------------+ +| **default**: | ``30`` | ++--------------+-------------+ +| **unit**: | ``seconds`` | ++--------------+-------------+ + +Configure timeout on blocking read/write operations when executing a command in a SSH connection. + +``OPENWISP_SSH_CONNECTION_TIMEOUT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``int`` | ++--------------+-------------+ +| **default**: | ``5`` | ++--------------+-------------+ +| **unit**: | ``seconds`` | ++--------------+-------------+ + +Configure timeout for the TCP connect when establishing a SSH connection. + +``OPENWISP_CONNECTORS`` +~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------------------------------------------------------------------------------------+ +| **type**: | ``tuple`` | ++--------------+------------------------------------------------------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | ( | +| | ('openwisp_controller.connection.connectors.ssh.Ssh', 'SSH'), | +| | ('openwisp_controller.connection.connectors.openwrt.snmp.OpenWRTSnmp', 'OpenWRT SNMP'), | +| | ('openwisp_controller.connection.connectors.airos.snmp.AirOsSnmp', 'Ubiquiti AirOS SNMP'), | +| | ) | ++--------------+------------------------------------------------------------------------------------------------+ + +Available connector classes. Connectors are python classes that specify ways +in which OpenWISP can connect to devices in order to launch commands. + +``OPENWISP_UPDATE_STRATEGIES`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------------------------------------------------------------------------------------+ +| **type**: | ``tuple`` | ++--------------+----------------------------------------------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | ( | +| | ('openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt', 'OpenWRT SSH'), | +| | ) | ++--------------+----------------------------------------------------------------------------------------+ + +Available update strategies. An update strategy is a subclass of a +connector class which defines an ``update_config`` method which is +in charge of updating the configuration of the device. + +This operation is launched in a background worker when the configuration +of a device is changed. + +It's possible to write custom update strategies and add them to this +setting to make them available in OpenWISP. + +``OPENWISP_CONFIG_UPDATE_MAPPING`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------------------------------------------------------+ +| **type**: | ``dict`` | ++--------------+--------------------------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | { | +| | 'netjsonconfig.OpenWrt': OPENWISP_UPDATE_STRATEGIES[0][0], | +| | } | ++--------------+--------------------------------------------------------------------+ + +A dictionary that maps configuration backends to update strategies in order to +automatically determine the update strategy of a device connection if the +update strategy field is left blank by the user. + +``OPENWISP_CONTROLLER_BACKENDS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------------------------------------------+ +| **type**: | ``tuple`` | ++--------------+-----------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | ( | +| | ('netjsonconfig.OpenWrt', 'OpenWRT'), | +| | ('netjsonconfig.OpenWisp', 'OpenWISP'), | +| | ) | ++--------------+-----------------------------------------------+ + +Available configuration backends. For more information, see `netjsonconfig backends +`_. + +``OPENWISP_CONTROLLER_VPN_BACKENDS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------------------------------------------------------------------------------+ +| **type**: | ``tuple`` | ++--------------+----------------------------------------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | ( | +| | ('openwisp_controller.vpn_backends.OpenVpn', 'OpenVPN'), | +| | ('openwisp_controller.vpn_backends.Wireguard', 'WireGuard'), | +| | ('openwisp_controller.vpn_backends.VxlanWireguard', 'VXLAN over WireGuard'), | +| | ('openwisp_controller.vpn_backends.ZeroTier', 'ZeroTier'), | +| | ) | ++--------------+----------------------------------------------------------------------------------+ + +Available VPN backends for VPN Server objects. For more information, see `netjsonconfig VPN backends +`_. + +A VPN backend must follow some basic rules in order to be compatible with *openwisp-controller*: + +* it MUST allow at minimum and at maximum one VPN instance +* the main *NetJSON* property MUST match the lowercase version of the class name, + eg: when using the ``OpenVpn`` backend, the system will look into + ``config['openvpn']`` +* it SHOULD focus on the server capabilities of the VPN software being used + +``OPENWISP_CONTROLLER_DEFAULT_BACKEND`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------------------------------------+ +| **type**: | ``str`` | ++--------------+----------------------------------------+ +| **default**: | ``OPENWISP_CONTROLLER_BACKENDS[0][0]`` | ++--------------+----------------------------------------+ + +The preferred backend that will be used as initial value when adding new ``Config`` or +``Template`` objects in the admin. + +This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_BACKENDS`` setting, +which is ``netjsonconfig.OpenWrt``. + +Setting it to ``None`` will force the user to choose explicitly. + +``OPENWISP_CONTROLLER_DEFAULT_VPN_BACKEND`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------------------------------+ +| **type**: | ``str`` | ++--------------+--------------------------------------------+ +| **default**: | ``OPENWISP_CONTROLLER_VPN_BACKENDS[0][0]`` | ++--------------+--------------------------------------------+ + +The preferred backend that will be used as initial value when adding new ``Vpn`` objects in the admin. + +This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_VPN_BACKENDS`` setting, +which is ``openwisp_controller.vpn_backends.OpenVpn``. + +Setting it to ``None`` will force the user to choose explicitly. + +``OPENWISP_CONTROLLER_REGISTRATION_ENABLED`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Whether devices can automatically register through the controller or not. + +This feature is enabled by default. + +Autoregistration must be supported on the devices in order to work, see `openwisp-config automatic +registration `_ for more information. + +``OPENWISP_CONTROLLER_CONSISTENT_REGISTRATION`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Whether devices that are already registered are recognized when reflashed or reset, hence keeping +the existing configuration without creating a new one. + +This feature is enabled by default. + +Autoregistration must be enabled also on the devices in order to work, see `openwisp-config +consistent key generation `_ +for more information. + +``OPENWISP_CONTROLLER_REGISTRATION_SELF_CREATION`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +Whether devices that are not already present in the system are allowed to register or not. + +Turn this off if you still want to use auto-registration to avoid having to +manually set the device UUID and key in its configuration file but also want +to avoid indiscriminate registration of new devices without explicit permission. + +``OPENWISP_CONTROLLER_CONTEXT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------+ +| **type**: | ``dict`` | ++--------------+------------------+ +| **default**: | ``{}`` | ++--------------+------------------+ + +Additional context that is passed to the default context of each device object. + +``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide configuration variables. + +For more information regarding how to use configuration variables in OpenWISP, +see `How to use configuration variables <#how-to-use-configuration-variables>`_. + +For technical information about how variables are handled in the lower levels +of OpenWISP, see `netjsonconfig context: configuration variables +`_. + +``OPENWISP_CONTROLLER_DEFAULT_AUTO_CERT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+---------------------------+ +| **type**: | ``bool`` | ++--------------+---------------------------+ +| **default**: | ``True`` | ++--------------+---------------------------+ + +The default value of the ``auto_cert`` field for new ``Template`` objects. + +The ``auto_cert`` field is valid only for templates which have ``type`` +set to ``VPN`` and indicates whether configuration regarding the VPN tunnel is +provisioned automatically to each device using the template, eg: + +- when using OpenVPN, new `x509 `_ certificates + will be generated automatically using the same CA assigned to the related VPN object +- when using WireGuard, new pair of private and public keys + (using `Curve25519 `_) will be generated, as well as + an IP address of the subnet assigned to the related VPN object +- when using `VXLAN `_ tunnels over Wireguad, + in addition to the configuration generated for WireGuard, a new VID will be generated + automatically for each device if the configuration option "auto VNI" is turned on in + the VPN object + +All these auto generated configuration options will be available as +template variables. + +The objects that are automatically created will also be removed when they are not +needed anymore (eg: when the VPN template is removed from a configuration object). + +``OPENWISP_CONTROLLER_CERT_PATH`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+---------------------------+ +| **type**: | ``str`` | ++--------------+---------------------------+ +| **default**: | ``/etc/x509`` | ++--------------+---------------------------+ + +The filesystem path where x509 certificate will be installed when +downloaded on routers when ``auto_cert`` is being used (enabled by default). + +``OPENWISP_CONTROLLER_COMMON_NAME_FORMAT`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------------------+ +| **type**: | ``str`` | ++--------------+------------------------------+ +| **default**: | ``{mac_address}-{name}`` | ++--------------+------------------------------+ + +Defines the format of the ``common_name`` attribute of VPN client certificates +that are automatically created when using VPN templates which have ``auto_cert`` +set to ``True``. A unique slug generated using `shortuuid `_ +is appended to the common name to introduce uniqueness. Therefore, resulting +common names will have ``{OPENWISP_CONTROLLER_COMMON_NAME_FORMAT}-{unique-slug}`` +format. + +**Note:** If the ``name`` and ``mac address`` of the device are equal, +the ``name`` of the device will be omitted from the common name to avoid redundancy. + +``OPENWISP_CONTROLLER_MANAGEMENT_IP_DEVICE_LIST`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------------------+ +| **type**: | ``bool`` | ++--------------+------------------------------+ +| **default**: | ``True`` | ++--------------+------------------------------+ + +In the device list page, the column ``IP`` will show the ``management_ip`` if +available, defaulting to ``last_ip`` otherwise. + +If this setting is set to ``False`` the ``management_ip`` won't be shown +in the device list page even if present, it will be shown only in the device +detail page. + +You may set this to ``False`` if for some reason the majority of your user +doesn't care about the management ip address. + +``OPENWISP_CONTROLLER_CONFIG_BACKEND_FIELD_SHOWN`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------------------+ +| **type**: | ``bool`` | ++--------------+------------------------------+ +| **default**: | ``True`` | ++--------------+------------------------------+ + +This setting toggles the ``backend`` fields in add/edit pages in Device and Template configuration, +as well as the ``backend`` field/filter in Device list and Template list. + +If this setting is set to ``False`` these items will be removed from the UI. + +Note: This setting affects only the configuration backend and NOT the VPN backend. + +``OPENWISP_CONTROLLER_DEVICE_NAME_UNIQUE`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +This setting conditionally enforces unique Device names in an Organization. +The query to enforce this is case-insensitive. + +Note: For this constraint to be optional, it is enforced on an application level and not on database. + +``OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``False`` | ++--------------+-------------+ + +The field ``hardware_id`` can be used to store a unique hardware id, for example a serial number. + +If this setting is set to ``True`` then this field will be shown first in the device list page +and in the add/edit device page. + +This feature is disabled by default. + +``OPENWISP_CONTROLLER_HARDWARE_ID_OPTIONS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------------------------------------------------+ +| **type**: | ``dict`` | ++--------------+--------------------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | { | +| | 'blank': not OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED, | +| | 'null': True, | +| | 'max_length': 32, | +| | 'unique': True, | +| | 'verbose_name': _('Serial number'), | +| | 'help_text': _('Serial number of this device') | +| | } | ++--------------+--------------------------------------------------------------+ + +Options for the model field ``hardware_id``. + +* ``blank``: wether the field is allowed to be blank +* ``null``: wether an empty value will be stored as ``NULL`` in the database +* ``max_length``: maximum length of the field +* ``unique``: wether the value of the field must be unique +* ``verbose_name``: text for the human readable label of the field +* ``help_text``: help text to be displayed with the field + +``OPENWISP_CONTROLLER_HARDWARE_ID_AS_NAME`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +When the hardware ID feature is enabled, devices will be referenced with +their hardware ID instead of their name. + +If you still want to reference devices by their name, set this to ``False``. + +``OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------------------------+ +| **type**: | ``tuple`` | ++--------------+----------------------------+ +| **default**: | ``('Device', 'Devices')`` | ++--------------+----------------------------+ + +Defines the ``verbose_name`` attribute of the ``Device`` model, which is displayed in the +admin site. The first and second element of the tuple represent the singular and plural forms. + +For example, if we want to change the verbose name to "Hotspot", we could write: + +.. code-block:: python + + OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME = ('Hotspot', 'Hotspots') + +``OPENWISP_CONTROLLER_HIDE_AUTOMATICALLY_GENERATED_SUBNETS_AND_IPS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``bool`` | ++--------------+-----------+ +| **default**: | ``False`` | ++--------------+-----------+ + +Setting this to ``True`` will hide subnets and IPs generated using `subnet division rules <#subnet-division-app>`_ +from being displayed on the changelist view of Subnet and IP admin. + +``OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+---------------------------------------------------------------------------------------------------------+ +| **type**: | ``tuple`` | ++--------------+---------------------------------------------------------------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | ( | +| | ('openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType', 'Device'), | +| | ('openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType', 'VPN'), | +| | ) | +| | | ++--------------+---------------------------------------------------------------------------------------------------------+ + +`Available types for Subject Division Rule <#device-subnet-division-rule>`_ objects. +For more information on how to write your own types, read +`"Custom Subnet Division Rule Types" section of this documentation <#custom-subnet-division-rule-types>`_ + +``OPENWISP_CONTROLLER_API`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``bool`` | ++--------------+-----------+ +| **default**: | ``True`` | ++--------------+-----------+ + +Indicates whether the API for Openwisp Controller is enabled or not. +To disable the API by default add `OPENWISP_CONTROLLER_API = False` in `settings.py` file. + +``OPENWISP_CONTROLLER_API_HOST`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``str`` | ++--------------+-----------+ +| **default**: | ``None`` | ++--------------+-----------+ + +Allows to specify backend URL for API requests, if the frontend is hosted separately. + +``OPENWISP_CONTROLLER_USER_COMMANDS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------+ +| **type**: | ``list`` | ++--------------+----------+ +| **default**: | ``[]`` | ++--------------+----------+ + +Allows to specify a ``list`` of tuples for adding commands as described in +`'How to define custom commands" <#how-to-define-new-options-in-the-commands-menu>`_ section. + +``OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------------------------------------+ +| **type**: | ``dict`` | ++--------------+------------------------------------------------+ +| **default**: | .. code-block:: python | +| | | +| | { | +| | # By default all commands are allowed | +| | '__all__': '*', | +| | } | +| | | ++--------------+------------------------------------------------+ + +This setting controls the command types that are enabled on the system +By default, all command types are enabled to all the organizations, +but it's possible to disable a specific command for a specific organization +as shown in the following example: + +.. code-block:: python + + OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS = { + '__all__': '*', + # Organization UUID: # Tuple of enabled commands + '7448a190-6e65-42bf-b8ea-bb6603e593a5': ('reboot', 'change_password'), + } + +In the example above, the organization with UUID ``7448a190-6e65-42bf-b8ea-bb6603e593a5`` +will allow to send only commands of type ``reboot`` and ``change_password``, +while all the other organizations will have all command types enabled. + +``OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+------------------------------------------+ +| **type**: | ``dict`` | ++--------------+------------------------------------------+ +| **default**: | ``{'type': 'object', 'properties': {}}`` | ++--------------+------------------------------------------+ + +Allows specifying JSONSchema used for validating meta-data of `Device Group <#device-groups>`__. + +``OPENWISP_CONTROLLER_SHARED_MANAGEMENT_IP_ADDRESS_SPACE`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------+ +| **type**: | ``bool`` | ++--------------+----------+ +| **default**: | ``True`` | ++--------------+----------+ + +By default, the system assumes that the address space of the management +tunnel is shared among all the organizations using the system, that is, +the system assumes there's only one management VPN, tunnel or other +networking technology to reach the devices it controls. + +When set to ``True``, any device belonging to any +organization will never have the same ``management_ip`` as another device, +the latest device declaring the management IP will take the IP and any +other device who declared the same IP in the past will have the field +reset to empty state to avoid potential conflicts. + +Set this to ``False`` if every organization has its dedicated management +tunnel with a dedicated address space that is reachable by the OpenWISP server. + +``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-------------+ +| **type**: | ``bool`` | ++--------------+-------------+ +| **default**: | ``True`` | ++--------------+-------------+ + +By default, only the management IP will be used to establish connection +with 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``. + +``OPENWISP_CONTROLLER_DSA_OS_MAPPING`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------+ +| **type**: | ``dict`` | ++--------------+----------+ +| **default**: | ``{}`` | ++--------------+----------+ + +OpenWISP Controller can figure out whether it should use the new OpenWrt syntax +for DSA interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by +reading the ``os`` field of the ``Device`` object. However, if the firmware you +are using has a custom firmware identifier, the system will not be able to figure +out whether it should use the new syntax and it will default to +`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK <#>`_. + +If you want to make sure the system can parse your custom firmware +identifier properly, you can follow the example below. + +For the sake of the example, the OS identifier ``MyCustomFirmware 2.0`` +corresponds to ``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` corresponds to +``OpenWrt 21.02``. Configuring this setting as indicated below will allow +OpenWISP to supply the right syntax automatically. + +Example: + +.. code-block:: python + + OPENWISP_CONTROLLER_DSA_OS_MAPPING = { + 'netjsonconfig.OpenWrt': { + # OpenWrt >=21.02 configuration syntax will be used for + # these OS identifiers. + '>=21.02': [r'MyCustomFirmware 2.1(.*)'], + # OpenWrt <=21.02 configuration syntax will be used for + # these OS identifiers. + '<21.02': [r'MyCustomFirmware 2.0(.*)'] + } + } + +**Note**: The OS identifier should be a regular expression as shown in above example. + +``OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+----------+ +| **type**: | ``bool`` | ++--------------+----------+ +| **default**: | ``True`` | ++--------------+----------+ + +The value of this setting decides whether to use DSA syntax +(OpenWrt >=21 configuration syntax) if openwisp-controller fails +to make that decision automatically. + +``OPENWISP_CONTROLLER_GROUP_PIE_CHART`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``bool`` | ++--------------+-----------+ +| **default**: | ``False`` | ++--------------+-----------+ + +Allows to show a pie chart like the one in the screenshot. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/devicegroups-piechart.png + :alt: device groups piechart + +Active groups are groups which have at least one device in them, +while emtpy groups do not have any device assigned. + +``OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+-----------+ +| **type**: | ``dict`` | ++--------------+-----------+ +| **default**: | see below | ++--------------+-----------+ + +.. code-block:: python + + # default value of OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS: + + dict( + max_retries=5, # total number of retries + retry_backoff=True, # exponential backoff + retry_backoff_max=600, # 10 minutes + retry_jitter=True, # randomness into exponential backoff + ) + + +This setting is utilized by background API tasks executed +by `ZeroTier VPN servers and ZeroTier VPN clients <#how-to-setup-zerotier-tunnels>`_ to handle recoverable +HTTP status codes such as 429, 500, 502, 503, and 504. These tasks are retried with a maximum +of 5 attempts with an exponential backoff and jitter, with a maximum delay of 10 minutes. + +This feature ensures that ZeroTier Service API calls +are resilient to recoverable failures, improving the reliability of the system. + +For more information on these settings, you can refer to the `the celery documentation regarding automatic retries +for known errors. `_ diff --git a/docs/user/templates-and-variables.rst b/docs/user/templates-and-variables.rst new file mode 100644 index 000000000..d108a0d76 --- /dev/null +++ b/docs/user/templates-and-variables.rst @@ -0,0 +1,193 @@ +Template and Variables +---------------------- + +Default Templates +~~~~~~~~~~~~~~~~~ + +When templates are flagged as default, they will be automatically assigned to new devices. + +If there are multiple default templates, these are assigned to the device in alphabetical +order based on their names, for example, given the following default templates: + +- Access +- Interfaces +- SSH Keys + +They will be assigned to devices in exactly that order. + +If for some technical reason (eg: one default template depends on the presence of another +default template which must be assigned earlier) you need to change the ordering, you can +simply rename the templates by prefixing them with numbers, eg: + +- 1 Interfaces +- 2. SSH Keys +- 3. Access + +Required Templates +~~~~~~~~~~~~~~~~~~ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png + :alt: Required template example + +Required templates are similar to `Default templates <#default-templates>`__ +but cannot be unassigned from a device configuration, they can only be overridden. + +They will be always assigned earlier than default templates, +so they can be overridden if needed. + +In the example above, the "SSID" template is flagged as "(required)" +and its checkbox is always checked and disabled. + +How to use configuration variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes the configuration is not exactly equal on all the devices, +some parameters are unique to each device or need to be changed +by the user. + +In these cases it is possible to use configuration variables in conjunction +with templates, this feature is also known as *configuration context*, think of +it like a dictionary which is passed to the function which renders the +configuration, so that it can fill variables according to the passed context. + +The different ways in which variables are defined are described below in +the order (high to low) of their precedence: + +1. `User defined device variables <#user-defined-device-variables>`_ +2. `Predefined device variables <#predefined-device-variables>`_ +3. `Group variables <#group-variables>`_ +4. `Organization variables <#organization-variables>`_ +5. `Global variables <#global-variables>`_ +6. `Template default values <#template-default-values>`_ + +User defined device variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the device configuration section you can find a section named +"Configuration variables" where it is possible to define the configuration +variables and their values, as shown in the example below: + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png + :alt: context + +Predefined device variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each device gets the following attributes passed as configuration variables: + +* ``id`` +* ``key`` +* ``name`` +* ``mac_address`` + +Group variables +~~~~~~~~~~~~~~~ + +Variables can also be defined in `Device groups <#device-groups>`__. + +Refer the `Group configuration variables `_ +section for detailed information. + +Organization variables +~~~~~~~~~~~~~~~~~~~~~~ + +Variables can also be defined at the organization level. + +You can set the *organization variables* from the organization change page +``/admin/openwisp_users/organization//change/``, under the +**Configuration Management Settings**. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png + :alt: organization variables + +Global variables +~~~~~~~~~~~~~~~~ + +Variables can also be defined globally using the +`OPENWISP_CONTROLLER_CONTEXT <#openwisp-controller-context>`_ setting. + +Template default values +~~~~~~~~~~~~~~~~~~~~~~~ + +It's possible to specify the default values of variables defined in a template. + +This allows to achieve 2 goals: + +1. pass schema validation without errors (otherwise it would not be possible + to save the template in the first place) +2. provide good default values that are valid in most cases but can be + overridden in the device if needed + +These default values will be overridden by the +`User defined device variables <#user-defined-device-variables>`_. + +The default values of variables can be manipulated from the section +"configuration variables" in the edit template page: + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png + :alt: default values + +System defined variables +~~~~~~~~~~~~~~~~~~~~~~~~ + +Predefined device variables, global variables and other variables that +are automatically managed by the system (eg: when using templates of +type VPN-client) are displayed in the admin UI as *System Defined Variables* +in read-only mode. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png + :alt: system defined variables + +**Note:** `Group configuration variables <#group-configuration-variables>`__ +are also added to the **System Defined Variables** of the device. + +Example usage of variables +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here's a typical use case, the WiFi SSID and WiFi password. +You don't want to define this for every device, but you may want to +allow operators to easily change the SSID or WiFi password for a +specific device without having to re-define the whole wifi interface +to avoid duplicating information. + +This would be the template: + +.. code-block:: json + + { + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "access_point", + "radio": "radio0", + "ssid": "{{wlan0_ssid}}", + "encryption": { + "protocol": "wpa2_personal", + "key": "{{wlan0_password}}", + "cipher": "auto" + } + } + } + ] + } + +These would be the default values in the template: + +.. code-block:: json + + { + "wlan0_ssid": "SnakeOil PublicWiFi", + "wlan0_password": "Snakeoil_pwd!321654" + } + +The default values can then be overridden at +`device level <#user-defined-device-variables>`_ if needed, eg: + +.. code-block:: json + + { + "wlan0_ssid": "Room 23 ACME Hotel", + "wlan0_password": "room_23pwd!321654" + } diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst new file mode 100644 index 000000000..622d56b9a --- /dev/null +++ b/docs/user/zerotier.rst @@ -0,0 +1,108 @@ +How to setup ZeroTier Tunnels +----------------------------- + +Follow the procedure described below to setup ZeroTier tunnels on your devices. + +**Note:** This example uses **Shared systemwide (no organization)** option as +the organization for VPN server and VPN client template. You can use any +organization as long as VPN server, VPN client template and Device has same +organization. + +1. Configure Self-Hosted ZeroTier Network Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you haven't already set up a self-hosted Zerotier network controller on your server, +now is a good time to do so. You can start by simply installing Zerotier on your server +from the `official website `_. + +2. Create VPN server configuration for ZeroTier +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. +2. We will set **Name** of this VPN server ``ZeroTier`` and **Host** as + ``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN server). +3. Select ``ZeroTier`` from the dropdown as **VPN Backend**. +4. When using ZeroTier, OpenWISP takes care of managing IP addresses + (assigning an IP address to each VPN clients (Zerotier network members). + You can create a new subnet or select an existing one from the dropdown menu. + You can also assign an **Internal IP** to the Zerotier controller or + leave it empty for OpenWISP to configure. This IP address will be used + to assign it to the Zerotier controller running on the server. +5. Set the **Webhook AuthToken**, this will be ZeroTier authorization token which you + can obtain by running the following command on the ZeroTier controller: + + .. code-block:: shell + + sudo cat /var/lib/zerotier-one/authtoken.secret + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png + :alt: ZeroTier VPN server configuration example 1 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png + :alt: ZeroTier VPN server configuration example 2 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png + :alt: ZeroTier VPN server configuration example 3 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png + :alt: ZeroTier VPN server configuration example 4 + +6. After clicking on **Save and continue editing**, OpenWISP automatically detects + the node address of the Zerotier controller and creates a Zerotier network. + The **network_id** of this network can be viewed in the **System Defined Variables** + section, where it also provides internal IP address information. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png + :alt: ZeroTier VPN server configuration example 5 + +3. Create VPN client template for ZeroTier VPN Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Visit ``/admin/config/template/add/`` to add a new template. +2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and + select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can + be applied to. For this example, we will leave it to ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here + it is ``ZeroTier``. +5. Ensure that the **Automatic tunnel provisioning** option is checked. + This will enable OpenWISP to automatically provision an IP address and + ZeroTier identity secrets (used for assigning member IDs) for each ZeroTier VPN client. +6. After clicking on **Save and continue editing** button, you will see details + of *ZeroTier* VPN server in **System Defined Variables**. The template + configuration will be automatically generated which you can tweak + accordingly. We will use the automatically generated VPN client configuration + for this example. + +**Note:** OpenWISP uses `zerotier-idtool +`_ +to manage **ZeroTier identity secrets**. Please make sure that you have +`ZeroTier package installed `_ on the server. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png + :alt: ZeroTier VPN client template example + +4. Apply ZeroTier VPN template to devices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Note**: This step assumes that you already have a device registered on +OpenWISP. Register or create a device before proceeding. + +1. Open the **Configuration** tab of the concerned device. +2. Select the *ZeroTier Client* template. +3. Upon clicking the **Save and Continue Editing** button, you will see entries + in the **System Defined Variables** section. These entries will include **zerotier_member_id**, **identity_secret**, + and the internal **IP address** of the ZeroTier client (network member) on the device, along with details of the VPN server. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png + :alt: ZeroTier VPN device configuration example 1 + +4. Once the configuration is successfully applied to the device, you will notice a new ZeroTier interface + that is up and running. This interface will have the name ``owzt89f498`` (where ``owzt`` is followed + by the last six hexadecimal characters of the ZeroTier **network ID**). + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png + :alt: ZeroTier VPN device configuration example 2 + +**Voila!** You have successfully configured OpenWISP +to manage ZeroTier tunnels for your devices. From dd94b9a04503edb7f329b5a4d0a5e981b7ac3d7e Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 26 Apr 2024 17:25:04 +0530 Subject: [PATCH 02/44] [chores] Fixed typo --- docs/developer/developer-docs.rst | 2 +- docs/developer/extending.rst | 2 +- docs/developer/installation.rst | 2 +- docs/developer/project-structure.rst | 2 +- docs/developer/signals.rst | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst index 5405e7d0c..35ef06a27 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/extending.rst b/docs/developer/extending.rst index f7334beea..b1790ff7e 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,7 +1,7 @@ Extending openwisp-controller ----------------------------- -.. include:: /paritals/developers-docs-warning.rst +.. include:: /partials/developers-docs-warning.rst One of the core values of the OpenWISP project is `Software Reusability `_, diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 28208770f..7df32c4f7 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,7 +1,7 @@ Developer installation instructions ----------------------------------- -.. include:: /paritals/developers-docs-warning.rst +.. include:: /partials/developers-docs-warning.rst Dependencies ~~~~~~~~~~~~ diff --git a/docs/developer/project-structure.rst b/docs/developer/project-structure.rst index 0b4d4c6b1..f29c469db 100644 --- a/docs/developer/project-structure.rst +++ b/docs/developer/project-structure.rst @@ -1,7 +1,7 @@ Project Structure & main features ---------------------------------- -.. include:: /paritals/developers-docs-warning.rst +.. include:: /partials/developers-docs-warning.rst OpenWISP Controller is a python package consisting of four django apps: diff --git a/docs/developer/signals.rst b/docs/developer/signals.rst index 568956a84..19dfd61f0 100644 --- a/docs/developer/signals.rst +++ b/docs/developer/signals.rst @@ -1,7 +1,7 @@ Signals ------- -.. include:: /paritals/developers-docs-warning.rst +.. include:: /partials/developers-docs-warning.rst ``config_modified`` ~~~~~~~~~~~~~~~~~~~ From 93afaeb99cfa318b8e067609fb17b03868d9e56f Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 26 Apr 2024 19:39:00 +0530 Subject: [PATCH 03/44] [chores] Fixed references --- docs/developer/extending.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index b1790ff7e..43ab48519 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -739,9 +739,9 @@ 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 `documentation section about +For more information, see the relevant :ref:`documentation section about registering notification types in openwisp-notifications -`_. +`_. Once a new notification type is registered, you have to use the `"notify" signal provided in openwisp-notifications From b82d8f87d11bec6a21973d08f681536c0f71f5d9 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Tue, 14 May 2024 19:34:00 -0400 Subject: [PATCH 04/44] [docs] Restructured docs (part 1) --- CHANGES.rst | 2 + CONTRIBUTING.rst | 2 + README.rst | 12 +- docs/developer/developer-docs.rst | 12 -- docs/developer/extending.rst | 14 +- docs/developer/index.rst | 16 ++ docs/developer/installation.rst | 102 ++++----- docs/developer/{signals.rst => utils.rst} | 93 +++++++- docs/index.rst | 34 +++ docs/overview.rst | 59 ----- docs/partials/developer-docs.rst | 13 ++ docs/user/device-groups.rst | 91 ++++---- docs/user/import-export.rst | 30 +++ .../project-structure.rst => user/intro.rst} | 65 +++--- docs/user/notification-alerts.rst | 10 - docs/user/organization-limits.rst | 13 +- ...gure-push-updates.rst => push-updates.rst} | 24 ++- docs/user/rest-api.rst | 2 + docs/user/settings.rst | 11 +- .../{send-commands.rst => shell-commands.rst} | 58 ++--- docs/user/templates.rst | 202 ++++++++++++++++++ ...plates-and-variables.rst => variables.rst} | 140 +++++------- 22 files changed, 637 insertions(+), 368 deletions(-) delete mode 100644 docs/developer/developer-docs.rst create mode 100644 docs/developer/index.rst rename docs/developer/{signals.rst => utils.rst} (67%) create mode 100644 docs/index.rst delete mode 100644 docs/overview.rst create mode 100644 docs/partials/developer-docs.rst create mode 100644 docs/user/import-export.rst rename docs/{developer/project-structure.rst => user/intro.rst} (72%) delete mode 100644 docs/user/notification-alerts.rst rename docs/user/{how-to-configure-push-updates.rst => push-updates.rst} (88%) rename docs/user/{send-commands.rst => shell-commands.rst} (69%) create mode 100644 docs/user/templates.rst rename docs/user/{templates-and-variables.rst => variables.rst} (53%) diff --git a/CHANGES.rst b/CHANGES.rst index 899d22b6d..305c1bd47 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,5 @@ +:orphan: + Changelog ========= diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 79c907b8c..d7868e7e3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1 +1,3 @@ +:orphan: + Please refer to the `Contribution Guidelines `_. diff --git a/README.rst b/README.rst index 17aa278fa..21ec3a43a 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +:orphan: + openwisp-controller =================== @@ -78,13 +80,11 @@ see the **Want to help OpenWISP?** `Find out how to help us grow here `_. ------------- - -.. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 +Documentation +------------- ------------- +- Developer documentation (TODO: add link) +- User documentation (TODO: add link) Contributing ------------ diff --git a/docs/developer/developer-docs.rst b/docs/developer/developer-docs.rst deleted file mode 100644 index 35ef06a27..000000000 --- a/docs/developer/developer-docs.rst +++ /dev/null @@ -1,12 +0,0 @@ -Developers Documentation ------------------------- - -.. include:: /partials/developers-docs-warning.rst - -.. toctree:: - :maxdepth: 1 - - ./installation.rst - ./project-structure.rst - ./signals.rst - ./extending.rst diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 43ab48519..2be9524ff 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,7 +1,7 @@ Extending openwisp-controller ----------------------------- -.. include:: /partials/developers-docs-warning.rst +.. include:: ../partials/developer-docs.rst One of the core values of the OpenWISP project is `Software Reusability `_, @@ -736,14 +736,4 @@ setting as follows: 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 :ref:`documentation section about -registering notification types 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. +Refer to ... diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 000000000..76e8496c4 --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,16 @@ +Developer Docs +============== + +.. 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/installation.rst b/docs/developer/installation.rst index 7df32c4f7..60855353d 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,43 +1,16 @@ -Developer installation instructions ------------------------------------ +Developer Installation Instructions +=================================== -.. include:: /partials/developers-docs-warning.rst +.. include:: ../partials/dev-docs-warn.rst Dependencies -~~~~~~~~~~~~ +------------ -* Python >= 3.7 +* Python >= 3.8 * OpenSSL -Install stable version from pypi -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install from pypi: - -.. code-block:: shell - - pip install openwisp-controller - -Install development version -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install tarball: - -.. code-block:: shell - - pip install https://github.com/openwisp/openwisp-controller/tarball/master - -Alternatively you can install via pip using git: - -.. code-block:: shell - - pip install -e git+git://github.com/openwisp/openwisp-controller#egg=openwisp_controller - -If you want to contribute, follow the instructions in -`Installing for development <#installing-for-development>`_. - Installing for development -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- Install the system dependencies: @@ -66,14 +39,16 @@ Launch Redis and PostgreSQL: docker-compose up -d redis postgres -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 your base python packages are up to date +before moving to the next step: .. code-block:: shell @@ -126,12 +101,42 @@ Run quality assurance tests with: ./run-qa-checks -Install and run on docker -~~~~~~~~~~~~~~~~~~~~~~~~~ +Alternative sources +------------------- + +Pypi +~~~~ + +To install the latest stable version from pypi: + +.. code-block:: shell + + pip install openwisp-controller + +Github +~~~~~~ + +To install the latest development version tarball via HTTPs: + +.. code-block:: shell + + pip install https://github.com/openwisp/openwisp-controller/tarball/master + +Alternatively you can use the git protocol: + +.. code-block:: shell + + pip install -e git+git://github.com/openwisp/openwisp-controller#egg=openwisp_controller + +Install and run on Docker +------------------------- + +.. warning:: + + This Docker image is for development purposes only. -NOTE: 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: @@ -146,23 +151,24 @@ Run the docker container: docker-compose up Troubleshooting steps for common installation issues -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------------- You may encounter some issues while installing GeoDjango. Unable to load SpatiaLite library extension? -############################################ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you are getting below exception:: +If you are incurring in the following exception:: django.core.exceptions.ImproperlyConfigured: Unable to load the SpatiaLite library extension -then, You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as explained in +You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` +as explained in `django documentation regarding how to install and configure spatialte `_. Having Issues with other geospatial libraries? -############################################## +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Please refer `troubleshooting issues related to geospatial libraries @@ -170,7 +176,7 @@ Please refer .. important:: - If you want to add ``openwisp-controller`` in an existing Django - project, then you can take reference from the - `test project in openwisp-controller repository + If you want to add OpenWISP Controller to an + existing Django project, then you can refer to the + `test project in the openwisp-controller repository `_ diff --git a/docs/developer/signals.rst b/docs/developer/utils.rst similarity index 67% rename from docs/developer/signals.rst rename to docs/developer/utils.rst index 19dfd61f0..b1d7d4f3e 100644 --- a/docs/developer/signals.rst +++ b/docs/developer/utils.rst @@ -1,8 +1,15 @@ +Code Utilities +============== + +.. include:: ../partials/developer-docs.rst + +.. contents:: + :depth: 2 + :local: + Signals ------- -.. include:: /partials/developers-docs-warning.rst - ``config_modified`` ~~~~~~~~~~~~~~~~~~~ @@ -61,6 +68,8 @@ object are changed, but only on ``post_add`` or ``post_remove`` actions, ``post_clear`` is ignored for the same reason explained in the previous section. +.. _config_backend_changed: + ``config_backend_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -182,10 +191,11 @@ The signal is emitted when the device group changes. It is not emitted when the device is created. +.. _group_templates_changed: + ``group_templates_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - **Path**: ``openwisp_controller.config.signals.group_templates_changed`` **Arguments**: @@ -237,3 +247,80 @@ The signal is emitted when the peers of VPN server gets changed. It is only emitted for ``Vpn`` object with **WireGuard** or **VXLAN over WireGuard** backend. + +.. _registering_unregistering_commands: + +Registering / Unregistering Commands +------------------------------------ + +OpenWISP Controller allows to register new command options +or unregister existing command options +through two utility functions: + +- ``openwisp_controller.connection.commands.register_command`` +- ``openwisp_controller.connection.commands.unregister_command`` + +You can use these functions to register new custom commands +or unregister existing commands from your code. + +.. note:: + + These functions are to be used as an alternative to + the :ref:`OPENWISP_CONTROLLER_USER_COMMANDS` setting + when :doc:`extending openwisp-controller ` + or when developing custom applications based on OpenWISP Controller. + +``register_command`` +~~~~~~~~~~~~~~~~~~~~ + ++--------------------+------------------------------------------------------------------+ +| Parameter | Description | ++--------------------+------------------------------------------------------------------+ +| ``command_name`` | A ``str`` defining identifier for the command. | ++--------------------+------------------------------------------------------------------+ +| ``command_config`` | A ``dict`` defining configuration of the command | +| | as shown in `^Command Configuration^ <~command-configuration>`_. | ++--------------------+------------------------------------------------------------------+ + +**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already +registered with the same name. + +``unregister_command`` +~~~~~~~~~~~~~~~~~~~~~~ + ++--------------------+-----------------------------------------+ +| Parameter | Description | ++--------------------+-----------------------------------------+ +| ``command_name`` | A ``str`` defining name of the command. | ++--------------------+-----------------------------------------+ + +**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not exists. + +Controller Notifications +------------------------ + +The notification types registered and used by OpenWISP Controller are +listed in the following table. + ++-----------------------+----------------------------------------------------------------------+ +| Notification Type | Use | ++-----------------------+----------------------------------------------------------------------+ +| ``config_error`` | Fires when the status of a device configuration changes to ``error``.| ++-----------------------+----------------------------------------------------------------------+ +| ``device_registered`` | Fires when a new device registers itself. | ++-----------------------+----------------------------------------------------------------------+ + +Registering notification types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can define your own notification types using +``register_notification_type`` function from OpenWISP Notifications. + +For more information, see the relevant :ref:`documentation section about +registering notification types 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/index.rst b/docs/index.rst new file mode 100644 index 000000000..f52751778 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,34 @@ +Controller +========== + +OpenWISP Controller is responsible of of managing +the core resources of the network +and allows automating several aspects like +adoption, provisioning, VPN tunnel configuration, +generation of X509 certificates, +subnet and IP address allocation and more. + +.. toctree:: + :caption: User Docs + :maxdepth: 1 + + user/intro.rst + user/templates.rst + user/variables.rst + user/device-groups.rst + user/push-updates.rst + user/shell-commands.rst + user/import-export.rst + user/organization-limits.rst + user/how-to-setup-wireguard.rst + user/how-to-setup-vxlan-over-wireguard.rst + user/zerotier.rst + user/automatic-provisioning-of-subnets.rst + user/rest-api.rst + user/settings.rst + +.. toctree:: + :caption: Developer Docs + :maxdepth: 2 + + Developer Docs Index diff --git a/docs/overview.rst b/docs/overview.rst deleted file mode 100644 index 49fb305ab..000000000 --- a/docs/overview.rst +++ /dev/null @@ -1,59 +0,0 @@ -OpenWISP Controller -=================== - -.. note:: - - This is the latest version - -OpenWISP Controller is a configuration manager that allows to automate several -networking tasks like adoption, provisioning, management VPN configuration, -X509 certificates automatic generation, revocation of x509 certificates and -a lot more 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-monitoring `_: - provides device status monitoring, collection of metrics, charts, alerts, - possibility to define custom checks -- `openwisp-firmware-upgrader `_: - automated firmware upgrades (single devices 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-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-ipam `_: - allows to manage the assignment of IP addresses used in the network -- `openwisp-notifications `_: - allows users to be aware of important events happening in the network. - -**For a more complete overview of the OpenWISP modules and architecture**, -see the -`OpenWISP Architecture Overview -`_. - - -.. toctree:: - :maxdepth: 1 - :glob: - - user/automatic-provisioning-of-subnets.rst - user/device-groups.rst - user/how-to-configure-push-updates.rst - user/how-to-setup-vxlan-over-wireguard.rst - user/how-to-setup-wireguard.rst - user/notification-alerts.rst - user/organization-limits.rst - user/send-commands.rst - user/templates-and-variables.rst - user/zerotier.rst - user/rest-api.rst - user/settings.rst - developer/developer-docs.rst diff --git a/docs/partials/developer-docs.rst b/docs/partials/developer-docs.rst new file mode 100644 index 000000000..50ddc51ee --- /dev/null +++ b/docs/partials/developer-docs.rst @@ -0,0 +1,13 @@ +.. note:: + + This documentation page is aimed at developers who want + to customize, change or extend the code of OpenWISP Controller + 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 Controller User Docs ` diff --git a/docs/user/device-groups.rst b/docs/user/device-groups.rst index 0cfc2c9f9..81f884361 100644 --- a/docs/user/device-groups.rst +++ b/docs/user/device-groups.rst @@ -1,17 +1,23 @@ Device Groups ------------- -Device Groups provide features aimed at adding specific management rules -for the devices of an organization: +Device groups allow to group similar devices together, +the groups usually share not only a common characteristic but also some +kind of organizational need: they need to have specific configuration +templates, variables and/or associated metadata which differs from the +rest of the network. -- Group similar devices by having dedicated groups for access points, routers, etc. -- Define `group metadata <~group-metadata>`_. -- Define `group configuration templates <~group-templates>`_. -- Define `group configuration variables <~group-configuration-variables>`__. +Features provided by Device Groups: + +.. contents:: + :depth: 2 + :local: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png :alt: Device Group example +.. _device_group_templates: + Group Templates ~~~~~~~~~~~~~~~ @@ -28,38 +34,44 @@ important points: needed. - If a device group is changed, the system will automatically remove the group templates of the old group and apply the new templates of the new - group (this operation is implemented by leveraging the - `group_templates_changed <~group_templates_changed>`_ signal). + group (this operation is implemented by leveraging + the :ref:`group_templates_changed` signal). - If the group templates are changed, the devices which belong to the group will be automatically updated to reflect the changes (this operation is executed in a background task). - In case the configuration backend of a device is changed, the system will handle this automatically too and update the group - templates accordingly (this operation is implemented by leveraging the - `config_backend_changed <~config_backend_changed>`_ signal). + templates accordingly (this operation is implemented by + leveraging the :ref:`config_backend_changed` signal). - If a device does not have a configuration defined yet, but it is assigned to a group which has templates defined, the system will automatically - create a configuration for it using the default backend specified in - `OPENWISP_CONTROLLER_DEFAULT_BACKEND <~OPENWISP_CONTROLLER_DEFAULT_BACKEND>`_ setting. + create a configuration for it using the default backend specified + in the :ref:`OPENWISP_CONTROLLER_DEFAULT_BACKEND` setting. **Note:** the list of templates shown in the edit group page do not -contain templates flagged as ^default^ or ^required^ to avoid redundancy -because those templates are automatically assigned by the system +contain templates flagged +as :ref:`"default" ` +or :ref:`"required" ` +to avoid redundancy because those templates +are automatically assigned by the system to new devices. This feature works also when editing group templates or the group assigned -to a device via the `REST API <~change-device-group-detail>`__. +to a device via the +:ref:`REST API `. + +.. _device_group_variables: Group Configuration Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Groups allow to define configuration variables which are automatically added to the device's context in the **System Defined Variables**. -Check the `^How to use configuration variables^ section <~how-to-use-configuration-variables>`_ -to learn about precedence of different configuration variables. +Check the :doc:`./variables` section +to learn more about precedence of different configuration variables. -This feature works also when editing group templates or the group assigned -to a device via the `REST API <~change-device-group-detail>`__. +This feature also works when editing group templates or the group assigned +to a device via the :ref:`REST API `. Group Metadata ~~~~~~~~~~~~~~ @@ -67,37 +79,18 @@ Group Metadata Groups allow to store additional information regarding a group in the structured metadata field (which can be accessed via the REST API). -The metadata field allows custom structure and validation to standardize -information across all groups using the -`^OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA^ <~openwisp-controller-device-group-schema>`_ -setting. - -**Note:** *Group configuration variables* and *Group metadata* serves different purposes. -The group configuration variables should be used when the device configuration is required -to be changed for particular group of devices. Group metadata should be used to store -additional data for the devices. Group metadata is not used for configuration generation. +The metadata field allows custom structure and validation to +standardize information across all groups using +the :ref:`OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA` setting. -Export/Import Device data -------------------------- +Variables vs Metadata +~~~~~~~~~~~~~~~~~~~~~ -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png - :alt: Import / Export +*Group configuration variables* and *Group metadata* serves different purposes. -The device list page offers two buttons to export and import device data in -different formats. - -The export feature respects any filters selected in the device list. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :alt: Export - -For importing devices into the system, only the required fields are needed, -for example, the following CSV file will import a device named -``TestImport`` with mac address ``00:11:22:09:44:55`` in the organization with -UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: - - organization,name,mac_address - 3cb5e18c-0312-48ab-8dbd-038b8415bd6f,TestImport,00:11:22:09:44:55 +The group configuration variables should be used when the device configuration is required +to be changed for particular group of devices. -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png - :alt: Import / Export +Group metadata should be used to store additional data for the device +group, this data can be fetched and/or tweaked via the REST API if needed. +Group metadata is not designed to be used for configuration purposes. diff --git a/docs/user/import-export.rst b/docs/user/import-export.rst new file mode 100644 index 000000000..14299fc31 --- /dev/null +++ b/docs/user/import-export.rst @@ -0,0 +1,30 @@ +Export/Import Device data +========================= + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png + :alt: Import / Export + +The device list page offers two buttons to export and import device data in +different formats. + +Exporting +--------- + +The export feature respects any filters selected in the device list. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :alt: Export + +Importing +--------- + +For importing devices into the system, only the required fields are needed, +for example, the following CSV file will import a device named +``TestImport`` with mac address ``00:11:22:09:44:55`` in the organization with +UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: + + organization,name,mac_address + 3cb5e18c-0312-48ab-8dbd-038b8415bd6f,TestImport,00:11:22:09:44:55 + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png + :alt: Import / Export diff --git a/docs/developer/project-structure.rst b/docs/user/intro.rst similarity index 72% rename from docs/developer/project-structure.rst rename to docs/user/intro.rst index f29c469db..35fa1bdb0 100644 --- a/docs/developer/project-structure.rst +++ b/docs/user/intro.rst @@ -1,12 +1,18 @@ -Project Structure & main features ----------------------------------- +Project Structure & Main Features +================================= -.. include:: /partials/developers-docs-warning.rst +OpenWISP Controller is a Python package +which ships five Django apps. -OpenWISP Controller is a python package consisting of four django apps: +.. contents:: + :depth: 1 + :local: Config App -~~~~~~~~~~ +---------- + +The config app is the core of the controller module and implements all +the following features: * **configuration management** for embedded devices supporting different firmwares: - `OpenWRT `_ @@ -24,26 +30,30 @@ Config App * **simple HTTP resources**: allow devices to automatically download configuration updates * **VPN management**: `automatically provision VPN tunnels <#openwisp-controller-default-auto-cert>`_, including cryptographic keys, IP addresses -* `REST API <#rest-api-reference>`_ -* `Export/Import devices <#>`_ +* `Import/Export devices <#>`_ + +It exposes different `REST API endpoints <#rest-api-reference>`_. PKI App -~~~~~~~ +------- The PKI app is based on `django-x509 `_, it allows to create, import and view x509 CAs and certificates directly from -the administration dashboard, it also adds different endpoints to the -`REST API <#rest-api-reference>`_. +the administration dashboard. + +It exposes different `REST API endpoints <#rest-api-reference>`_. Connection App -~~~~~~~~~~~~~~ +-------------- This app allows OpenWISP Controller to use different protocols to reach network devices. Currently, the default connnection protocols are SSH and SNMP, but the protocol mechanism is extensible and more protocols can be implemented if needed. +It exposes different `REST API endpoints <#rest-api-reference>`_. + SSH -### +~~~ The SSH connector allows the controller to initialize connections to the devices in order perform `push operations <#how-to-configure-push-updates>`__: @@ -51,7 +61,6 @@ in order perform `push operations <#how-to-configure-push-updates>`__: - Sending configuration updates. - `Executing shell commands <#sending-commands-to-devices>`_. - Perform `firmware upgrades via the additional firmware upgrade module `_. -- `REST API <#rest-api-reference>`_ The default connection protocol implemented is SSH, but other protocol mechanism is extensible and custom protocols can be implemented as well. @@ -62,35 +71,39 @@ Access via SSH key is recommended, the SSH key algorithms supported are: - Ed25519 SNMP -#### +~~~~ The SNMP connector is useful to collect monitoring information and it's used in `openwisp-monitoring`_ for performing checks to collect monitoring information. `Read more `_ on how to use it. Geo App -~~~~~~~ +------- The geographic app is based on `django-loci `_ and allows to define the geographic coordinates of the devices, as well as their indoor coordinates on floorplan images. -It also adds different endpoints to the `REST API <#rest-api-reference>`_. +It exposes different `REST API endpoints <#rest-api-reference>`_. Subnet Division App -~~~~~~~~~~~~~~~~~~~ +------------------- + +.. note:: -This app allows to automatically provision subnets and IP addresses which will be -available as `system defined configuration variables <#system-defined-variables>`_ -that can be used in templates. The purpose of this app is to allow users to automatically -provision and configure specific -subnets and IP addresses to the devices without the need of manual intervention. + This app is optional, if you don't need it you + can avoid adding it to ``settings.INSTALLED_APPS``. + +This app allows to automatically provision subnets and IP +addresses which will be available as +`system defined configuration variables <#system-defined-variables>`_ +that can be used in templates. + +The purpose of this app is to allow users to automatically +provision and configure specific subnets and IP addresses +to the devices without the need of manual intervention. Refer to `"How to configure automatic provisioning of subnets and IPs" section of this documentation <#how-to-configure-automatic-provisioning-of-subnets-and-ips>`_ to learn about features provided by this app. - -This app is optional, if you don't need it you can avoid adding it to -``settings.INSTALLED_APPS``. - diff --git a/docs/user/notification-alerts.rst b/docs/user/notification-alerts.rst deleted file mode 100644 index 3879a09d8..000000000 --- a/docs/user/notification-alerts.rst +++ /dev/null @@ -1,10 +0,0 @@ -Default Alerts / Notifications ------------------------------- - -+-----------------------+---------------------------------------------------------------------+ -| Notification Type | Use | -+-----------------------+---------------------------------------------------------------------+ -| ``config_error`` | Fires when status of a device configuration changes to ``error``. | -+-----------------------+---------------------------------------------------------------------+ -| ``device_registered`` | Fires when a new device is registered automatically on the network. | -+-----------------------+---------------------------------------------------------------------+ diff --git a/docs/user/organization-limits.rst b/docs/user/organization-limits.rst index 2adbbadcf..0b146e28c 100644 --- a/docs/user/organization-limits.rst +++ b/docs/user/organization-limits.rst @@ -1,11 +1,16 @@ Organization Limits ------------------- -Allows configuring following limits for each organization: +You can restrict the number of devices managed by each organization. -- Limit number of devices managed by the organization. +To set these limits: -You can change the limits from the organization's admin page: +1. Navigate to **USERS & ORGANIZATIONS** on the left-hand navigation menu. +2. Go to **Organizations**. +3. Click on the specific organization you want to limit. +4. In the **CONTROLLER LIMIT** section, set the desired limit. + +Refer to the screenshot below for guidance: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png - :alt: Organization limits + :alt: Organization limits diff --git a/docs/user/how-to-configure-push-updates.rst b/docs/user/push-updates.rst similarity index 88% rename from docs/user/how-to-configure-push-updates.rst rename to docs/user/push-updates.rst index cc6a1a978..9bd45fad7 100644 --- a/docs/user/how-to-configure-push-updates.rst +++ b/docs/user/push-updates.rst @@ -1,16 +1,19 @@ -How to configure push updates ------------------------------ +Configuring Push updates +------------------------ + +**Note**: If you have installed OpenWISP with +`ansbile-openwisp2 role `_ +then you can skip the following steps. + +The Ansible role automatically creates a default template +to update ``authorized_keys`` on networking devices using the +default access credentials. Follow the procedure described below to enable secure SSH access from OpenWISP to your devices, this is required to enable push updates (whenever the configuration is changed, OpenWISP will trigger the update in the background) and/or -`firmware upgrades (via the additional module openwisp-firmware-upgrader) -`_. - -**Note**: If you have installed OpenWISP with `openwisp2 Ansbile role `_ -then you can skip the following steps. The Ansible role automatically creates a -default template to update ``authorized_keys`` on networking devices using the -default access credentials. +:doc:`firmware upgrades (via the additional +module openwisp-firmware-upgrader) <../../../openwisp-firmware-upgrader/docs/index>` 1. Generate SSH key ~~~~~~~~~~~~~~~~~~~ @@ -22,7 +25,8 @@ used by OpenWISP to access the devices, to do so, you can use the following comm echo './sshkey' | ssh-keygen -t ed25519 -C "openwisp" -This will create two files in the current directory, one called ``sshkey`` (the private key) and one called +This will create two files in the current directory, one called ``sshkey`` +(the private key) and one called ``sshkey.pub`` (the public key). Store the content of these files in a secure location. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index f3592b54f..0b4e1322f 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -363,6 +363,8 @@ Get device group detail GET /api/v1/controller/group/{id}/ +.. _change_device_group_detail: + Change device group detail ########################## diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 05e869df4..6d24101db 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -1,8 +1,7 @@ Settings -------- -You can change the values for the following variables in -``settings.py`` to configure your instance of openwisp-controller. +.. include:: /partials/settings-note.rst ``OPENWISP_SSH_AUTH_TIMEOUT`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -158,6 +157,8 @@ A VPN backend must follow some basic rules in order to be compatible with *openw ``config['openvpn']`` * it SHOULD focus on the server capabilities of the VPN software being used +.. _OPENWISP_CONTROLLER_DEFAULT_BACKEND: + ``OPENWISP_CONTROLLER_DEFAULT_BACKEND`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -240,6 +241,8 @@ Turn this off if you still want to use auto-registration to avoid having to manually set the device UUID and key in its configuration file but also want to avoid indiscriminate registration of new devices without explicit permission. +.. _context_setting: + ``OPENWISP_CONTROLLER_CONTEXT`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -500,6 +503,8 @@ To disable the API by default add `OPENWISP_CONTROLLER_API = False` in `settings Allows to specify backend URL for API requests, if the frontend is hosted separately. +.. _OPENWISP_CONTROLLER_USER_COMMANDS: + ``OPENWISP_CONTROLLER_USER_COMMANDS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -544,6 +549,8 @@ In the example above, the organization with UUID ``7448a190-6e65-42bf-b8ea-bb660 will allow to send only commands of type ``reboot`` and ``change_password``, while all the other organizations will have all command types enabled. +.. _OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA: + ``OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/send-commands.rst b/docs/user/shell-commands.rst similarity index 69% rename from docs/user/send-commands.rst rename to docs/user/shell-commands.rst index a96b87f82..b8798eaae 100644 --- a/docs/user/send-commands.rst +++ b/docs/user/shell-commands.rst @@ -15,21 +15,23 @@ allows you to execute any command on the device as shown in the example below. :alt: Executing commands on device example **Note**: in order for this feature to work, a device needs to have at least -one **Access Credential** (see `How to configure push updates <~how-to-configure-push-updates>`__). +one **Access Credential** (see +:doc:`How to configure push updates `). The **Send Command** button will be hidden until the device has at least one **Access Credential**. If you need to allow your users to quickly send specific commands that are used often in your network regardless of your users' knowledge of Linux shell commands, you can add new commands -by following instructions in the `^How to define new options in the commands menu^ -<~how-to-define-new-options-in-the-commands-menu>`_ section below. +by following instructions in the :ref:`defining_new_menu_options` section below. If you are an advanced user and want to register commands programatically, then refer to `^Register / Unregistering commands^ <~registering--unregistering-commands>`_ section. -How to define new options in the commands menu -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _defining_new_menu_options: + +Defining new options in the commands menu +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Let's explore to define new custom commands to help users perform additional management actions @@ -150,43 +152,9 @@ passed as arguments to this callable. The example above includes a callable(``ping_command_callable``) for ``ping`` command. -Registering / Unregistering Commands -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -OpenWISP Controller provides registering and unregistering commands -through utility functions ``openwisp_controller.connection.commands.register_command`` -and ``openwisp_notifications.types.unregister_notification_type``. -You can use these functions to register or unregister commands -from your code. - -**Note**: These functions are to be used as an alternative to the -`^OPENWISP_CONTROLLER_USER_COMMANDS^ <~openwisp-controller-user-commands>`_ -when `developing custom modules based on openwisp-controller -<~extending-openwisp-controller>`_ or when developing custom third party -apps. - -``register_command`` -#################### - -+--------------------+------------------------------------------------------------------+ -| Parameter | Description | -+--------------------+------------------------------------------------------------------+ -| ``command_name`` | A ``str`` defining identifier for the command. | -+--------------------+------------------------------------------------------------------+ -| ``command_config`` | A ``dict`` defining configuration of the command | -| | as shown in `^Command Configuration^ <~command-configuration>`_. | -+--------------------+------------------------------------------------------------------+ - -**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already -registered with the same name. - -``unregister_command`` -###################### - -+--------------------+-----------------------------------------+ -| Parameter | Description | -+--------------------+-----------------------------------------+ -| ``command_name`` | A ``str`` defining name of the command. | -+--------------------+-----------------------------------------+ - -**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not exists. +How to register or unregister commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Refer to +:ref:`registering_unregistering_commands` +in the developer documentation. diff --git a/docs/user/templates.rst b/docs/user/templates.rst new file mode 100644 index 000000000..836f9a5fe --- /dev/null +++ b/docs/user/templates.rst @@ -0,0 +1,202 @@ +Configuration Templates +======================= + +Templates are designed to store configuration that can be reused by +some or all the devices in the system. + +Updating the configuration stored in a template allows to update the +configuration of all the devices that have that template assigned. + +This means that configuration can be defined only once for multiple +devices, and if the need to update a specific piece of configuration +arises, it can be easily achieved by updating the template. + +.. contents:: **Table of Contents**: + :backlinks: none + :depth: 3 + +Template ordering and override +------------------------------ + +A device can use multiple templates, **the order in which templates are +assigned to each device matters**: templates assigned last can override +templates assigned earlier, the order can be changed by drag and dropping +the template in the device configuration page as in the animated +screenshot below. + +.. image:: /images/templates/template-ordering.gif + :align: center + :alt: Template ordering: drag and drop to change order + +The device configuration can also override what is defined in templates. + +Overriding means redefining a specific configuration section in a way that +overwrites the template. + +**Overriding involves some form of duplication of information, which is +not great, it should be used as a last resort**. The recommended way to +define parts of the configuration that are specific for each device is to +use :doc:`Configuration variables <./variables>`. + +Shared templates vs organization specific +----------------------------------------- + +Templates can be *organization specific* or *shared* +(no organization specified). + +.. image:: /images/templates/organization-specific-vs-shared.gif + :align: center + :alt: Shared templates vs organization specific + +**Organization specific templates** will be available and usable only +within the same organization which they are assigned to. + +If no organization is specified when creating a template, a shared +template will be created, **shared templates are available to any +organization in the system**. + +Here are a few typical use cases of shared templates: + +- Management VPN +- Authorized SSH keys belonging to network administrators +- Crontab with generic periodic management operations + +.. _default_templates: + +Default Templates +----------------- + +.. image:: /images/templates/default-templates.gif + :align: center + :alt: Templates enabled by default + +When templates are flagged as **"Enabled by default"**, +they will be automatically assigned to new devices. + +This is a very powerful feature: **once default templates are correctly +configured to implement the use case you need, you will only have to +register a device into OpenWISP for it to auto-configure itself**. + +Moreover, you can change the default templates any time you need, which +is the reason this feature has replaced the practice of storing default +configuration in firmware images (which would need to be recompiled and +redistributed): with default templates, the default firmware image only +needs to contain the bare minimum configuration to connect to OpenWISP, +once the device connects to OpenWISP it will download and apply the +default templates without the need of manual intervention from +the network operators. + +An organization specific template flagged as default will be automatically +assigned to any new device which will be created in the same organization. + +A shared default template instead will be automatically assigned to all +the new devices which will be created in the system, regardless of +organization. + +.. _required_templates: + +Required Templates +------------------ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png + :alt: Required template example + +Required templates are similar to :ref:`default_templates` +but cannot be unassigned from a device configuration, +they can only be overridden. + +They will be always assigned earlier than default templates, +so they can be overridden if needed. + +In the example above, the "SSID" template is flagged as "(required)" +and its checkbox is always checked and disabled. + +Device Group Templates +---------------------- + +:ref:`default_templates` are an incredibly useful tool, +but they're limited: **only one set of default templates can be created** +per each organization. + +With :ref:`device_group_templates` it is possible to specify a +set of default templates for each device group. + +Template tags +------------- + +.. image:: /images/templates/template-tags.gif + :align: center + :alt: Template tags + + + +In some cases, you may have multiple set of default settings to use, +let's explain this with a practical example: you may have 2 different +device types in your network: + +- Mesh routers: they connect to one another, forming a + wireless mesh network +- Dumb access points: they connect to the mesh routers on the LAN + port and offer internet access which is routed via the mesh + network by the routers + +In this example case, the default configuration to use in each +device type can greatly differ. + +In such a setup, default templates would only contain configuration +which is common to both device types, while configuration which is +specific for each type would be stored in specific templates which +are then tagged with specific keywords: + +- ``mesh``: tag to use for mesh configuration +- ``dumb-ap``: tag to use for dumb AP configuration + +The `openwisp-config `_ +configuration of each device type must specify the correct tag before +each device registers in the system. + +Here's the sample ``/etc/config/openwisp`` configuration for mesh devices: + +.. code-block:: + + config controller 'http' + option url 'https://openwisp2.mynetwork.com' + option shared_secret 'mySharedSecret123' + option tags 'mesh' + +Once devices with the above configuration will register into the system, +any template tagged as ``mesh`` (as in the screenshot below) will be +assigned to them. + +.. image:: /images/templates/mesh-template-tag.png + :align: center + :alt: Template tags: mesh example + +The sample ``/etc/config/openwisp`` configuration for dumb access +points is the following: + +.. code-block:: + + config controller 'http' + option url 'https://openwisp2.mynetwork.com' + option shared_secret 'mySharedSecret123' + option tags 'dumb-ap' + +Once devices with the above configuration will register into the system, +any template tagged as ``dumb-ap`` (as in the screenshot below) +will be assigned to them. + +.. image:: /images/templates/dumb-ap-template-tag.png + :align: center + :alt: Template tags: dumb AP example + +Implementation details of templates +----------------------------------- + +Templates are implemented under the hood by the OpenWISP +configuration engine: netjsonconfig. + +For more advanced technical information about templates, consult the +netjsonconfig documentation: +`Basic Concepts, Template +`_. diff --git a/docs/user/templates-and-variables.rst b/docs/user/variables.rst similarity index 53% rename from docs/user/templates-and-variables.rst rename to docs/user/variables.rst index d108a0d76..4a4de9180 100644 --- a/docs/user/templates-and-variables.rst +++ b/docs/user/variables.rst @@ -1,67 +1,31 @@ -Template and Variables ----------------------- - -Default Templates -~~~~~~~~~~~~~~~~~ - -When templates are flagged as default, they will be automatically assigned to new devices. - -If there are multiple default templates, these are assigned to the device in alphabetical -order based on their names, for example, given the following default templates: - -- Access -- Interfaces -- SSH Keys - -They will be assigned to devices in exactly that order. - -If for some technical reason (eg: one default template depends on the presence of another -default template which must be assigned earlier) you need to change the ordering, you can -simply rename the templates by prefixing them with numbers, eg: - -- 1 Interfaces -- 2. SSH Keys -- 3. Access - -Required Templates -~~~~~~~~~~~~~~~~~~ - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png - :alt: Required template example - -Required templates are similar to `Default templates <#default-templates>`__ -but cannot be unassigned from a device configuration, they can only be overridden. - -They will be always assigned earlier than default templates, -so they can be overridden if needed. - -In the example above, the "SSID" template is flagged as "(required)" -and its checkbox is always checked and disabled. - -How to use configuration variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Configuration Variables +======================= Sometimes the configuration is not exactly equal on all the devices, some parameters are unique to each device or need to be changed by the user. -In these cases it is possible to use configuration variables in conjunction -with templates, this feature is also known as *configuration context*, think of +In these cases it is possible to use configuration variables in +conjunction with templates, this feature is also known as +*configuration context*, think of it like a dictionary which is passed to the function which renders the -configuration, so that it can fill variables according to the passed context. +configuration, so that it can fill variables according to the passed +context. + +Different types of variables +---------------------------- The different ways in which variables are defined are described below in -the order (high to low) of their precedence: +the order (high to low) of their precedence. -1. `User defined device variables <#user-defined-device-variables>`_ -2. `Predefined device variables <#predefined-device-variables>`_ -3. `Group variables <#group-variables>`_ -4. `Organization variables <#organization-variables>`_ -5. `Global variables <#global-variables>`_ -6. `Template default values <#template-default-values>`_ +.. contents:: + :depth: 2 + :local: -User defined device variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _user_defined_variables: + +1. User defined device variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the device configuration section you can find a section named "Configuration variables" where it is possible to define the configuration @@ -70,26 +34,26 @@ variables and their values, as shown in the example below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png :alt: context -Predefined device variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +2 Predefined device variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each device gets the following attributes passed as configuration variables: +Each device gets the following attributes passed as configuration +variables: * ``id`` * ``key`` * ``name`` * ``mac_address`` -Group variables -~~~~~~~~~~~~~~~ +3. Group variables +~~~~~~~~~~~~~~~~~~ -Variables can also be defined in `Device groups <#device-groups>`__. +Variables can also be defined in :doc:`./device-groups`. -Refer the `Group configuration variables `_ -section for detailed information. +Refer to :ref:`device_group_variables` for more information. -Organization variables -~~~~~~~~~~~~~~~~~~~~~~ +4. Organization variables +~~~~~~~~~~~~~~~~~~~~~~~~~ Variables can also be defined at the organization level. @@ -100,26 +64,29 @@ You can set the *organization variables* from the organization change page .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png :alt: organization variables -Global variables -~~~~~~~~~~~~~~~~ +5. Global variables +~~~~~~~~~~~~~~~~~~~ Variables can also be defined globally using the -`OPENWISP_CONTROLLER_CONTEXT <#openwisp-controller-context>`_ setting. +:ref:`context_setting` +setting, see also +:doc:`How to Edit Django Settings <../../../../user/django-settings>`. -Template default values -~~~~~~~~~~~~~~~~~~~~~~~ +6. Template default values +~~~~~~~~~~~~~~~~~~~~~~~~~~ -It's possible to specify the default values of variables defined in a template. +It's possible to specify the default values of variables +defined in a template. This allows to achieve 2 goals: -1. pass schema validation without errors (otherwise it would not be possible - to save the template in the first place) +1. pass schema validation without errors (otherwise it would not be + possible to save the template in the first place) 2. provide good default values that are valid in most cases but can be overridden in the device if needed These default values will be overridden by the -`User defined device variables <#user-defined-device-variables>`_. +:ref:`User defined device variables `. The default values of variables can be manipulated from the section "configuration variables" in the edit template page: @@ -127,22 +94,19 @@ The default values of variables can be manipulated from the section .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png :alt: default values -System defined variables -~~~~~~~~~~~~~~~~~~~~~~~~ +7. System defined variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Predefined device variables, global variables and other variables that are automatically managed by the system (eg: when using templates of -type VPN-client) are displayed in the admin UI as *System Defined Variables* -in read-only mode. +type VPN-client) are displayed in the admin UI as +*System Defined Variables* in read-only mode. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png :alt: system defined variables -**Note:** `Group configuration variables <#group-configuration-variables>`__ -are also added to the **System Defined Variables** of the device. - Example usage of variables -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- Here's a typical use case, the WiFi SSID and WiFi password. You don't want to define this for every device, but you may want to @@ -183,7 +147,8 @@ These would be the default values in the template: } The default values can then be overridden at -`device level <#user-defined-device-variables>`_ if needed, eg: +:ref:`device level ` +if needed, eg: .. code-block:: json @@ -191,3 +156,14 @@ The default values can then be overridden at "wlan0_ssid": "Room 23 ACME Hotel", "wlan0_password": "room_23pwd!321654" } + +Implementation details of variables +----------------------------------- + +Variables are implemented under the hood by the OpenWISP +configuration engine: netjsonconfig. + +For more advanced technical information about variables, consult the +netjsonconfig documentation: +`Basic Concepts, Context (configuration variables) +`_. From 82bf128a14e8b158221ab407d910e88c7605e4e4 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 15 May 2024 20:11:20 -0400 Subject: [PATCH 05/44] [docs] Improved Wireguard page --- docs/index.rst | 2 +- docs/user/how-to-setup-wireguard.rst | 101 ----------------------- docs/user/wireguard.rst | 119 +++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 102 deletions(-) delete mode 100644 docs/user/how-to-setup-wireguard.rst create mode 100644 docs/user/wireguard.rst diff --git a/docs/index.rst b/docs/index.rst index f52751778..3614ae4e1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ subnet and IP address allocation and more. user/shell-commands.rst user/import-export.rst user/organization-limits.rst - user/how-to-setup-wireguard.rst + user/wireguard.rst user/how-to-setup-vxlan-over-wireguard.rst user/zerotier.rst user/automatic-provisioning-of-subnets.rst diff --git a/docs/user/how-to-setup-wireguard.rst b/docs/user/how-to-setup-wireguard.rst deleted file mode 100644 index 06d09ca65..000000000 --- a/docs/user/how-to-setup-wireguard.rst +++ /dev/null @@ -1,101 +0,0 @@ -How to setup WireGuard tunnels ------------------------------- - -Follow the procedure described below to setup WireGuard tunnels on your devices. - -**Note:** This example uses **Shared systemwide (no organization)** option as -the organization for VPN server and VPN client template. You can use any -organization as long as VPN server, VPN client template and Device has same -organization. - -1. Create VPN server configuration for WireGuard -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. We will set **Name** of this VPN server ``Wireguard`` and **Host** as - ``wireguard-server.mydomain.com`` (update this to point to your - WireGuard VPN server). -3. Select ``WireGuard`` from the dropdown as **VPN Backend**. -4. When using WireGuard, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN peer). You can create a new subnet or - select an existing one from the dropdown menu. You can also assign an - **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to - configure. This IP address will be used by the WireGuard interface on - server. -5. We have set the **Webhook Endpoint** as ``https://wireguard-server.mydomain.com:8081/trigger-update`` - for this example. You will need to update this according to you VPN upgrader - endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be - used to ensure that configuration upgrades are requested from trusted - sources. - - **Note**: If you are following this tutorial for also setting up WireGuard - VPN server, just substitute ``wireguard-server.mydomain.com`` with hostname - of your VPN server and follow the steps in next section. - -6. Under the configuration section, set the name of WireGuard tunnel 1 interface. - We have used ``wg0`` in this example. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png - :alt: WireGuard VPN server configuration example 1 - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png - :alt: WireGuard VPN server configuration example 2 - -7. After clicking on **Save and continue editing**, you will see that OpenWISP - has automatically created public and private key for WireGuard server in - **System Defined Variables** along with internal IP address information. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png - :alt: WireGuard VPN server configuration example 3 - -2. Deploy Wireguard VPN Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you haven't already setup WireGuard on your VPN server, this will be a good -time do so. We recommend using the `ansible-wireguard-openwisp `_ -role for installing WireGuard since it also installs scripts that allows -OpenWISP to manage WireGuard VPN server. - -Pay attention to the VPN server attributes used in your playbook. It should be same as -VPN server configuration in OpenWISP. - -3. Create VPN client template for WireGuard VPN Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``Wireguard Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``Wireguard``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make - OpenWISP to automatically generate public and private keys and provision IP - address for each WireGuard VPN client. -6. After clicking on **Save and continue editing** button, you will see details - of *Wireguard* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration - for this example. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png - :alt: WireGuard VPN client template example - -4. Apply Wireguard VPN template to devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Note**: This step assumes that you already have a device registered on -OpenWISP. Register or create a device before proceeding. - -1. Open the **Configuration** tab of the concerned device. -2. Select the *WireGuard Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some - entries in **System Defined Variables**. It will contain internal IP address, - private and public key for the WireGuard client on the device along with - details of WireGuard VPN server. - -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png - :alt: WireGuard VPN device configuration example - -**Voila!** You have successfully configured OpenWISP to manage WireGuard -tunnels for your devices. diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst new file mode 100644 index 000000000..dd68cc35f --- /dev/null +++ b/docs/user/wireguard.rst @@ -0,0 +1,119 @@ +Automating WireGuard Tunnels +============================ + +This guide will help you to set up the automatic provisioning of +WireGuard tunnels for your devices. + +.. note:: + + This guide creates the VPN server and VPN client templates + as **Shared systemwide (no organization)** objects. This allows + any device of any organization to use the automation. + + If needed, you can use any organization as long as the VPN server, + the VPN client template, and devices have the same organization. + +1. Create VPN server configuration for WireGuard +------------------------------------------------ + +1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. +2. Set the **Name** of this VPN server as ``WireGuard`` and the + **Host** as ``wireguard-server.mydomain.com`` + (update this to point to your WireGuard VPN server). +3. Select ``WireGuard`` from the dropdown as the **VPN Backend**. +4. When using WireGuard, OpenWISP takes care of managing IP addresses, + assigning an IP address to each VPN peer. Create a new subnet or select + an existing one from the dropdown menu. You can also assign an + **Internal IP** to the WireGuard Server or leave it empty for + OpenWISP to configure. This IP address will be used by the WireGuard + interface on the server. +5. Set the **Webhook Endpoint** as + ``https://wireguard-server.mydomain.com:8081/trigger-update`` + for this example. Update this according to your VPN upgrader + endpoint. Set **Webhook AuthToken** to any strong passphrase; + this will be used to ensure that configuration upgrades are requested + from trusted sources. + + **Note**: If you are setting up a WireGuard VPN server, substitute + ``wireguard-server.mydomain.com`` with the hostname of your + VPN server and follow the steps in the next section. + +6. Under the configuration section, set the name of the WireGuard + tunnel 1 interface. In this example, we have used ``wg0``. + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png + :alt: WireGuard VPN server configuration example 1 + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png + :alt: WireGuard VPN server configuration example 2 + +7. After clicking on **Save and continue editing**, you will see + that OpenWISP has automatically created public and private keys + for the WireGuard server in **System Defined Variables**, + along with internal IP address information. + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png + :alt: WireGuard VPN server configuration example 3 + +2. Deploy WireGuard VPN Server +------------------------------ + +If you haven't already set up WireGuard on your VPN server, +this would be a good time to do so. + +We recommend using the +`ansible-wireguard-openwisp +`_ +role for installing WireGuard, as it also installs scripts that allow +OpenWISP to manage the WireGuard VPN server. + +Ensure that the VPN server attributes used in your playbook match +the VPN server configuration in OpenWISP. + +3. Create VPN client template for WireGuard VPN Server +------------------------------------------------------ + +1. Visit ``/admin/config/template/add/`` to add a new template. +2. Set ``WireGuard Client`` as **Name** (you can set whatever you want) and + select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can + be applied to. For this example, we will leave it to ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here + it is ``WireGuard``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will make + OpenWISP to automatically generate public and private keys and provision IP + address for each WireGuard VPN client. +6. After clicking on **Save and continue editing** button, you will see details + of *WireGuard* VPN server in **System Defined Variables**. The template + configuration will be automatically generated which you can tweak + accordingly. We will use the automatically generated VPN client configuration + for this example. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png + :alt: WireGuard VPN client template example + +4. Apply WireGuard VPN template to devices +------------------------------------------ + +.. note:: + + This step assumes that you already have a device registered on + OpenWISP. Register or create a device before proceeding. + +1. Open the **Configuration** tab of the concerned device. +2. Select the *WireGuard Client* template. +3. Upon clicking on **Save and continue editing** button, you will see some + entries in **System Defined Variables**. It will contain internal IP address, + private and public key for the WireGuard client on the device along with + details of WireGuard VPN server. + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png + :alt: WireGuard VPN device configuration example + +**Voila!** You have successfully configured OpenWISP to manage WireGuard +tunnels for your devices. From 7458e5008e2a25596c63f5c8269d521ff17f3e18 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 15 May 2024 20:11:42 -0400 Subject: [PATCH 06/44] [docs] Link to images --- docs/developer/installation.rst | 2 +- docs/user/device-groups.rst | 5 +++-- docs/user/import-export.rst | 15 +++++++++------ docs/user/organization-limits.rst | 3 ++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 60855353d..5fc764f76 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -179,4 +179,4 @@ Please refer If you want to add OpenWISP Controller to an existing Django project, then you can refer to the `test project in the openwisp-controller repository - `_ + `_. diff --git a/docs/user/device-groups.rst b/docs/user/device-groups.rst index 81f884361..c8dee1d0c 100644 --- a/docs/user/device-groups.rst +++ b/docs/user/device-groups.rst @@ -13,8 +13,9 @@ Features provided by Device Groups: :depth: 2 :local: -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png - :alt: Device Group example +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png + :alt: Device Group example .. _device_group_templates: diff --git a/docs/user/import-export.rst b/docs/user/import-export.rst index 14299fc31..3748e80ed 100644 --- a/docs/user/import-export.rst +++ b/docs/user/import-export.rst @@ -1,8 +1,9 @@ Export/Import Device data ========================= -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png - :alt: Import / Export +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png + :alt: Import / Export The device list page offers two buttons to export and import device data in different formats. @@ -12,8 +13,9 @@ Exporting The export feature respects any filters selected in the device list. -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :alt: Export +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :alt: Export Importing --------- @@ -26,5 +28,6 @@ UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: organization,name,mac_address 3cb5e18c-0312-48ab-8dbd-038b8415bd6f,TestImport,00:11:22:09:44:55 -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png - :alt: Import / Export +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png + :alt: Import / Export diff --git a/docs/user/organization-limits.rst b/docs/user/organization-limits.rst index 0b146e28c..74d556b02 100644 --- a/docs/user/organization-limits.rst +++ b/docs/user/organization-limits.rst @@ -12,5 +12,6 @@ To set these limits: Refer to the screenshot below for guidance: -.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png :alt: Organization limits From 661e74a4e919fe2f3e028ef8fafef0a243a27414 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 15 May 2024 21:10:38 -0400 Subject: [PATCH 07/44] [docs] Renamed VXLAN over wireguard --- docs/index.rst | 2 +- ...ow-to-setup-vxlan-over-wireguard.rst => vxlan-wireguard.rst} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/user/{how-to-setup-vxlan-over-wireguard.rst => vxlan-wireguard.rst} (100%) diff --git a/docs/index.rst b/docs/index.rst index 3614ae4e1..ba508e3c3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,7 +21,7 @@ subnet and IP address allocation and more. user/import-export.rst user/organization-limits.rst user/wireguard.rst - user/how-to-setup-vxlan-over-wireguard.rst + user/vxlan-wireguard.rst user/zerotier.rst user/automatic-provisioning-of-subnets.rst user/rest-api.rst diff --git a/docs/user/how-to-setup-vxlan-over-wireguard.rst b/docs/user/vxlan-wireguard.rst similarity index 100% rename from docs/user/how-to-setup-vxlan-over-wireguard.rst rename to docs/user/vxlan-wireguard.rst From 9bf12be35f2717127796ae759acb3250ec8b4c17 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 15 May 2024 21:13:13 -0400 Subject: [PATCH 08/44] [docs] Improved intro --- docs/index.rst | 3 ++ docs/user/intro.rst | 95 +++++++++++++++++++++++------------------ docs/user/settings.rst | 2 + docs/user/templates.rst | 4 +- docs/user/variables.rst | 2 + 5 files changed, 63 insertions(+), 43 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ba508e3c3..f4d3a10cb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,9 @@ adoption, provisioning, VPN tunnel configuration, generation of X509 certificates, subnet and IP address allocation and more. +For a full introduction please refer to +:doc:`user/intro`. + .. toctree:: :caption: User Docs :maxdepth: 1 diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 35fa1bdb0..7f4d3c7b3 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -1,5 +1,5 @@ -Project Structure & Main Features -================================= +Controller: Structure & Features +================================ OpenWISP Controller is a Python package which ships five Django apps. @@ -14,53 +14,65 @@ Config App The config app is the core of the controller module and implements all the following features: -* **configuration management** for embedded devices supporting different firmwares: +* **Configuration management** for embedded devices supporting: - `OpenWRT `_ - `OpenWISP Firmware `_ - - support for additional firmware can be added by `specifying custom backends <#netjsonconfig-backends>`_ -* **configuration editor** based on `JSON-Schema editor `_ -* **advanced edit mode**: edit `NetJSON `_ *DeviceConfiguration* objects for maximum flexibility -* `configuration templates `_: + - additional firmware can be added by + :ref:`specifying custom configuration + backends ` +* **Configuration editor** based on + `JSON-Schema editor `_ +* **Advanced edit mode**: edit + `NetJSON `_ + *DeviceConfiguration* objects for maximum flexibility +* :doc:`templates`: reduce repetition to the minimum, configure default and required templates -* `configuration variables <#how-to-use-configuration-variables>`_: - reference ansible-like variables in the configuration and templates -* **template tags**: tag templates to automate different types of auto-configurations (eg: mesh, WDS, 4G) -* **device groups**: add `devices to dedicated groups <#device-groups>`_ to - ease management of group of devices -* **simple HTTP resources**: allow devices to automatically download configuration updates -* **VPN management**: `automatically provision VPN tunnels <#openwisp-controller-default-auto-cert>`_, - including cryptographic keys, IP addresses -* `Import/Export devices <#>`_ - -It exposes different `REST API endpoints <#rest-api-reference>`_. +* :doc:`variables`: + reference variables in the configuration and templates +* :doc:`device-groups`: define different set of default configuration + and metadata in device groups +* :ref:`Template Tags `: define different + sets of default templates (eg: mesh, WDS, 4G) +* **HTTP resources**: allow devices to automatically check for and + download configuration updates +* **VPN management**: automatically provision VPN tunnel configurations, + including cryptographic keys and IP addresses, + eg: :doc:`OpenVPN `, :doc:`WireGuard ` +* :doc:`import-export` + +It exposes various :doc:`REST API endpoints `. PKI App ------- -The PKI app is based on `django-x509 `_, -it allows to create, import and view x509 CAs and certificates directly from -the administration dashboard. +The PKI app is based on `django-x509 +`_, allowing you to create, import, +and view x509 CAs and certificates directly from the administration +dashboard. -It exposes different `REST API endpoints <#rest-api-reference>`_. +It exposes various :doc:`REST API endpoints `. Connection App -------------- -This app allows OpenWISP Controller to use different protocols to reach network devices. -Currently, the default connnection protocols are SSH and SNMP, but the protocol -mechanism is extensible and more protocols can be implemented if needed. +This app enables OpenWISP Controller to use different protocols to reach +network devices. Currently, the default connection protocols are +SSH and SNMP, but the protocol mechanism is extensible, +allowing for implementation of additional protocols if needed. -It exposes different `REST API endpoints <#rest-api-reference>`_. +It exposes various :doc:`REST API endpoints `. SSH ~~~ -The SSH connector allows the controller to initialize connections to the devices -in order perform `push operations <#how-to-configure-push-updates>`__: +The SSH connector allows the controller to initialize connections +to the devices in order to perform +:doc:`push operations `, e.g.: - Sending configuration updates. -- `Executing shell commands <#sending-commands-to-devices>`_. -- Perform `firmware upgrades via the additional firmware upgrade module `_. +- :doc:`Executing shell commands `. +- Perform firmware upgrades via the additional + :doc:`firmware upgrade module `. The default connection protocol implemented is SSH, but other protocol mechanism is extensible and custom protocols can be implemented as well. @@ -73,18 +85,22 @@ Access via SSH key is recommended, the SSH key algorithms supported are: SNMP ~~~~ -The SNMP connector is useful to collect monitoring information and it's used in -`openwisp-monitoring`_ for performing checks to collect monitoring information. -`Read more `_ on how to use it. +The SNMP connector is useful to collect monitoring information +and it's used in +:doc:`OpenWISP Monitoring ` +for performing checks to collect monitoring information. +`Read more `_ +on how to use it. Geo App ------- -The geographic app is based on `django-loci `_ +The geographic app is based on +`django-loci `_ and allows to define the geographic coordinates of the devices, as well as their indoor coordinates on floorplan images. -It exposes different `REST API endpoints <#rest-api-reference>`_. +It exposes various :doc:`REST API endpoints `. Subnet Division App ------------------- @@ -96,14 +112,11 @@ Subnet Division App This app allows to automatically provision subnets and IP addresses which will be available as -`system defined configuration variables <#system-defined-variables>`_ -that can be used in templates. +:ref:`system defined configuration variables ` +that can be used in :doc:`templates`. The purpose of this app is to allow users to automatically provision and configure specific subnets and IP addresses to the devices without the need of manual intervention. -Refer to `"How to configure automatic provisioning of subnets and IPs" -section of this documentation -<#how-to-configure-automatic-provisioning-of-subnets-and-ips>`_ -to learn about features provided by this app. +Refer to :doc:`automatic-provisioning-of-subnets` for more information. diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 6d24101db..ffa9469e6 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -113,6 +113,8 @@ A dictionary that maps configuration backends to update strategies in order to automatically determine the update strategy of a device connection if the update strategy field is left blank by the user. +.. _OPENWISP_CONTROLLER_BACKENDS: + ``OPENWISP_CONTROLLER_BACKENDS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user/templates.rst b/docs/user/templates.rst index 836f9a5fe..fc704adc5 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -121,6 +121,8 @@ per each organization. With :ref:`device_group_templates` it is possible to specify a set of default templates for each device group. +.. _templates_tags: + Template tags ------------- @@ -128,8 +130,6 @@ Template tags :align: center :alt: Template tags - - In some cases, you may have multiple set of default settings to use, let's explain this with a practical example: you may have 2 different device types in your network: diff --git a/docs/user/variables.rst b/docs/user/variables.rst index 4a4de9180..9775e36ad 100644 --- a/docs/user/variables.rst +++ b/docs/user/variables.rst @@ -94,6 +94,8 @@ The default values of variables can be manipulated from the section .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png :alt: default values +.. _system_defined_variables: + 7. System defined variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 1d8829e09b75bdadec31c2f96ab4e91c7fffc573 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 15 May 2024 21:14:07 -0400 Subject: [PATCH 09/44] [docs] Renamed subnet division rule page --- docs/index.rst | 2 +- ...ic-provisioning-of-subnets.rst => subnet-division-rules.rst} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/user/{automatic-provisioning-of-subnets.rst => subnet-division-rules.rst} (100%) diff --git a/docs/index.rst b/docs/index.rst index f4d3a10cb..2608c4146 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ For a full introduction please refer to user/wireguard.rst user/vxlan-wireguard.rst user/zerotier.rst - user/automatic-provisioning-of-subnets.rst + user/subnet-division-rules.rst user/rest-api.rst user/settings.rst diff --git a/docs/user/automatic-provisioning-of-subnets.rst b/docs/user/subnet-division-rules.rst similarity index 100% rename from docs/user/automatic-provisioning-of-subnets.rst rename to docs/user/subnet-division-rules.rst From 3de0d2d895cde2dddfd0050ad110940aef27eb58 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 16 May 2024 13:58:43 -0400 Subject: [PATCH 10/44] [docs] Restructured zerotier, wireguard-vxlan, other details --- docs/developer/installation.rst | 2 +- docs/partials/shared-object.rst | 8 ++++ docs/user/intro.rst | 2 +- docs/user/vxlan-wireguard.rst | 73 ++++++++++++++++++--------------- docs/user/wireguard.rst | 11 +---- docs/user/zerotier.rst | 71 +++++++++++++++++++------------- 6 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 docs/partials/shared-object.rst diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 5fc764f76..81c716a41 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -1,7 +1,7 @@ Developer Installation Instructions =================================== -.. include:: ../partials/dev-docs-warn.rst +.. include:: ../partials/developer-docs.rst Dependencies ------------ diff --git a/docs/partials/shared-object.rst b/docs/partials/shared-object.rst new file mode 100644 index 000000000..fa84c4b16 --- /dev/null +++ b/docs/partials/shared-object.rst @@ -0,0 +1,8 @@ +.. note:: + + This guide creates the VPN server and VPN client templates + as **Shared systemwide (no organization)** objects. This allows + any device of any organization to use the automation. + + If needed, you can use any organization as long as the VPN server, + the VPN client template, and devices have the same organization. diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 7f4d3c7b3..34e46b233 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -119,4 +119,4 @@ The purpose of this app is to allow users to automatically provision and configure specific subnets and IP addresses to the devices without the need of manual intervention. -Refer to :doc:`automatic-provisioning-of-subnets` for more information. +Refer to :doc:`subnet-division-rules` for more information. diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index 16b09b594..b1b0538dd 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -1,16 +1,13 @@ -How to setup VXLAN over WireGuard tunnels ------------------------------------------ +Automating VXLAN over WireGuard tunnels +======================================= By following these steps, you will be able to setup layer 2 VXLAN tunnels encapsulated in WireGuard tunnels which work on layer 3. -**Note:** This example uses **Shared systemwide (no organization)** option as -the organization for VPN server and VPN client template. You can use any -organization as long as VPN server, VPN client template and Device has same -organization. +.. include:: ../partials/shared-object.rst 1. Create VPN server configuration for VXLAN over WireGuard -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------------------- 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. 2. We will set **Name** of this VPN server ``Wireguard VXLAN`` and **Host** as @@ -23,7 +20,8 @@ organization. **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to configure. This IP address will be used by the WireGuard interface on server. -5. We have set the **Webhook Endpoint** as ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` +5. We have set the **Webhook Endpoint** as + ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` for this example. You will need to update this according to you VPN upgrader endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be used to ensure that configuration upgrades are requested from trusted @@ -37,9 +35,11 @@ organization. We have used ``wg0`` in this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png :alt: WireGuard VPN VXLAN server configuration example 1 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png :alt: WireGuard VPN VXLAN server configuration example 2 7. After clicking on **Save and continue editing**, you will see that OpenWISP @@ -47,46 +47,50 @@ organization. **System Defined Variables** along with internal IP address information. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png :alt: WireGuard VXLAN VPN server configuration example 3 2. Deploy Wireguard VXLAN VPN Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------ -If you haven't already setup WireGuard on your VPN server, this will be a good -time do so. We recommend using the `ansible-wireguard-openwisp `_ -role for installing WireGuard since it also installs scripts that allows -OpenWISP to manage WireGuard VPN server along with VXLAN tunnels. +If you haven't already set up WireGuard on your VPN server, this is a good time to do so. +We recommend using the `ansible-wireguard-openwisp `_ +role for installing WireGuard since it also installs scripts that allow OpenWISP to manage +the WireGuard VPN server along with VXLAN tunnels. -Pay attention to the VPN server attributes used in your playbook. It should be same as -VPN server configuration in OpenWISP. +Pay attention to the VPN server attributes used in your playbook. It should be the same +as the VPN server configuration in OpenWISP. 3. Create VPN client template for WireGuard VXLAN VPN Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------------------ 1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``Wireguard VXLAN``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make - OpenWISP to automatically generate public and private keys and provision IP - address for each WireGuard VPN client along with VXLAN Network Indentifier(VNI). -6. After clicking on **Save and continue editing** button, you will see details - of *Wireguard VXLAN* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration - for this example. +2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and select + ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can be applied to. + For this example, we will leave it as ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is + ``Wireguard VXLAN``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP + automatically generate public and private keys and provision IP addresses for each + WireGuard VPN client along with the VXLAN Network Identifier (VNI). +6. After clicking on **Save and continue editing** button, you will see details of the + *Wireguard VXLAN* VPN server in **System Defined Variables**. The template + configuration will be automatically generated which you can tweak accordingly. We will + use the automatically generated VPN client configuration for this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png - :alt: WireGuard VXLAN VPN client template example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png + :alt: WireGuard VXLAN VPN client template example + 4. Apply Wireguard VXLAN VPN template to devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------ + +.. note:: -**Note**: This step assumes that you already have a device registered on -OpenWISP. Register or create a device before proceeding. + This step assumes that you already have a device registered on + OpenWISP. Register or create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *WireGuard VXLAN Client* template. @@ -96,6 +100,7 @@ OpenWISP. Register or create a device before proceeding. WireGuard VPN server along with VXLAN Network Identifier(VNI) of this device. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png :alt: WireGuard VXLAN VPN device configuration example **Voila!** You have successfully configured OpenWISP to manage VXLAN over diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst index dd68cc35f..cd676e038 100644 --- a/docs/user/wireguard.rst +++ b/docs/user/wireguard.rst @@ -2,16 +2,9 @@ Automating WireGuard Tunnels ============================ This guide will help you to set up the automatic provisioning of -WireGuard tunnels for your devices. +`WireGuard `_ tunnels for your devices. -.. note:: - - This guide creates the VPN server and VPN client templates - as **Shared systemwide (no organization)** objects. This allows - any device of any organization to use the automation. - - If needed, you can use any organization as long as the VPN server, - the VPN client template, and devices have the same organization. +.. include:: ../partials/shared-object.rst 1. Create VPN server configuration for WireGuard ------------------------------------------------ diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index 622d56b9a..ca3763830 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -1,34 +1,32 @@ -How to setup ZeroTier Tunnels ------------------------------ +Automating ZeroTier Tunnels +=========================== -Follow the procedure described below to setup ZeroTier tunnels on your devices. +Follow the procedure described below to set up +`ZeroTier `_ tunnels on your devices. -**Note:** This example uses **Shared systemwide (no organization)** option as -the organization for VPN server and VPN client template. You can use any -organization as long as VPN server, VPN client template and Device has same -organization. +.. include:: ../partials/shared-object.rst 1. Configure Self-Hosted ZeroTier Network Controller -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------------- -If you haven't already set up a self-hosted Zerotier network controller on your server, -now is a good time to do so. You can start by simply installing Zerotier on your server +If you haven't already set up a self-hosted ZeroTier network controller on your server, +now is a good time to do so. You can start by simply installing ZeroTier on your server from the `official website `_. 2. Create VPN server configuration for ZeroTier -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------- 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. 2. We will set **Name** of this VPN server ``ZeroTier`` and **Host** as ``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN server). 3. Select ``ZeroTier`` from the dropdown as **VPN Backend**. 4. When using ZeroTier, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN clients (Zerotier network members). + (assigning an IP address to each VPN client (ZeroTier network members)). You can create a new subnet or select an existing one from the dropdown menu. - You can also assign an **Internal IP** to the Zerotier controller or + You can also assign an **Internal IP** to the ZeroTier controller or leave it empty for OpenWISP to configure. This IP address will be used - to assign it to the Zerotier controller running on the server. -5. Set the **Webhook AuthToken**, this will be ZeroTier authorization token which you + to assign it to the ZeroTier controller running on the server. +5. Set the **Webhook AuthToken**, this will be the ZeroTier authorization token which you can obtain by running the following command on the ZeroTier controller: .. code-block:: shell @@ -36,27 +34,32 @@ from the `official website `_. sudo cat /var/lib/zerotier-one/authtoken.secret .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png :alt: ZeroTier VPN server configuration example 1 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png :alt: ZeroTier VPN server configuration example 2 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png :alt: ZeroTier VPN server configuration example 3 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png :alt: ZeroTier VPN server configuration example 4 6. After clicking on **Save and continue editing**, OpenWISP automatically detects - the node address of the Zerotier controller and creates a Zerotier network. - The **network_id** of this network can be viewed in the **System Defined Variables** + the node address of the ZeroTier controller and creates a ZeroTier network. + The **network_id** of this network can be viewed in the **System Defined Variables** section, where it also provides internal IP address information. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png :alt: ZeroTier VPN server configuration example 5 3. Create VPN client template for ZeroTier VPN Server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------------- 1. Visit ``/admin/config/template/add/`` to add a new template. 2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and @@ -71,22 +74,30 @@ from the `official website `_. 6. After clicking on **Save and continue editing** button, you will see details of *ZeroTier* VPN server in **System Defined Variables**. The template configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration + accordingly. + We will use the automatically generated VPN client configuration for this example. -**Note:** OpenWISP uses `zerotier-idtool -`_ -to manage **ZeroTier identity secrets**. Please make sure that you have -`ZeroTier package installed `_ on the server. +.. note:: + + OpenWISP uses `zerotier-idtool + `_ + to manage **ZeroTier identity secrets**. + Please make sure that you have + `ZeroTier package installed `_ + on the server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png - :alt: ZeroTier VPN client template example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png + :alt: ZeroTier VPN client template example 4. Apply ZeroTier VPN template to devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------- + +.. note:: -**Note**: This step assumes that you already have a device registered on -OpenWISP. Register or create a device before proceeding. + This step assumes that you already have a device registered on + OpenWISP. Register or create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *ZeroTier Client* template. @@ -95,6 +106,7 @@ OpenWISP. Register or create a device before proceeding. and the internal **IP address** of the ZeroTier client (network member) on the device, along with details of the VPN server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png :alt: ZeroTier VPN device configuration example 1 4. Once the configuration is successfully applied to the device, you will notice a new ZeroTier interface @@ -102,7 +114,8 @@ OpenWISP. Register or create a device before proceeding. by the last six hexadecimal characters of the ZeroTier **network ID**). .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png :alt: ZeroTier VPN device configuration example 2 -**Voila!** You have successfully configured OpenWISP -to manage ZeroTier tunnels for your devices. +**Congratulations!** You've successfully configured +OpenWISP to manage ZeroTier tunnels on your devices. From 8e75ddf0b1846de77baf81bf3156046c02852f62 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 16 May 2024 14:12:06 -0400 Subject: [PATCH 11/44] [docs] Renamed push-updates to push-operations --- docs/index.rst | 2 +- .../{push-updates.rst => push-operations.rst} | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) rename docs/user/{push-updates.rst => push-operations.rst} (89%) diff --git a/docs/index.rst b/docs/index.rst index 2608c4146..a1667f3c3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,7 +19,7 @@ For a full introduction please refer to user/templates.rst user/variables.rst user/device-groups.rst - user/push-updates.rst + user/push-operations.rst user/shell-commands.rst user/import-export.rst user/organization-limits.rst diff --git a/docs/user/push-updates.rst b/docs/user/push-operations.rst similarity index 89% rename from docs/user/push-updates.rst rename to docs/user/push-operations.rst index 9bd45fad7..ba695de49 100644 --- a/docs/user/push-updates.rst +++ b/docs/user/push-operations.rst @@ -1,19 +1,23 @@ -Configuring Push updates ------------------------- +Configuring Push Operations +--------------------------- -**Note**: If you have installed OpenWISP with -`ansbile-openwisp2 role `_ -then you can skip the following steps. +.. important:: + + If you have installed OpenWISP with the + `ansbile-openwisp2 role `_ + you can skip the following steps, which are handled automatically + by the ansible role during the first installation. The Ansible role automatically creates a default template to update ``authorized_keys`` on networking devices using the default access credentials. Follow the procedure described below to enable secure SSH access from OpenWISP to your -devices, this is required to enable push updates (whenever the configuration is changed, -OpenWISP will trigger the update in the background) and/or +devices, this is required to enable push operations +(whenever the configuration is changed, OpenWISP will trigger +the update in the background) and/or :doc:`firmware upgrades (via the additional -module openwisp-firmware-upgrader) <../../../openwisp-firmware-upgrader/docs/index>` +module openwisp-firmware-upgrader) <../../../openwisp-firmware-upgrader/docs/index>`. 1. Generate SSH key ~~~~~~~~~~~~~~~~~~~ From d65cce7c84b861873da237fdca07f1ff388aea59 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 16 May 2024 14:12:16 -0400 Subject: [PATCH 12/44] [docs] Link wireguard --- docs/user/vxlan-wireguard.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index b1b0538dd..c9aa1399e 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -1,8 +1,9 @@ -Automating VXLAN over WireGuard tunnels +Automating VXLAN over WireGuard Tunnels ======================================= By following these steps, you will be able to setup layer 2 VXLAN tunnels -encapsulated in WireGuard tunnels which work on layer 3. +encapsulated in `WireGuard `_ +tunnels which work on layer 3. .. include:: ../partials/shared-object.rst From 448ba011b6c357c8459c122db2f3da1011ddef28 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 16 May 2024 14:12:30 -0400 Subject: [PATCH 13/44] [docs] Import/Export consistency --- docs/user/import-export.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/user/import-export.rst b/docs/user/import-export.rst index 3748e80ed..28daeb8e6 100644 --- a/docs/user/import-export.rst +++ b/docs/user/import-export.rst @@ -1,4 +1,4 @@ -Export/Import Device data +Import/Export Device Data ========================= .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png @@ -8,15 +8,6 @@ Export/Import Device data The device list page offers two buttons to export and import device data in different formats. -Exporting ---------- - -The export feature respects any filters selected in the device list. - -.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :alt: Export - Importing --------- @@ -31,3 +22,12 @@ UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png :alt: Import / Export + +Exporting +--------- + +The export feature respects any filters selected in the device list. + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :alt: Export From e2602328f9911cd0b248ff16c39cad8afc82b8d2 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 16 May 2024 14:56:12 -0400 Subject: [PATCH 14/44] [docs] Restructured subnet division rule --- docs/user/subnet-division-rules.rst | 164 +++++++++++++++++----------- 1 file changed, 99 insertions(+), 65 deletions(-) diff --git a/docs/user/subnet-division-rules.rst b/docs/user/subnet-division-rules.rst index 6f09c31fa..162b70798 100644 --- a/docs/user/subnet-division-rules.rst +++ b/docs/user/subnet-division-rules.rst @@ -1,127 +1,149 @@ -How to configure automatic provisioning of subnets and IPs ----------------------------------------------------------- +Automating Subnet and IP Address Provisioning +============================================= -The following steps will help you configure automatic provisioning of subnets and IPs -for devices. +This guide helps you automate provisioning subnets +and IP addresses for your network devices. + +.. _step1_rule: 1. Create a Subnet and a Subnet Division Rule -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------- + +Create a master subnet. + +This is the parent subnet from which automatically +generated subnets will be provisioned. -Create a master subnet under which automatically generated subnets will be provisioned. +.. note:: -**Note**: Choose the size of the subnet appropriately considering your use case. + Choose a subnet size appropriate for the needs of your network. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png - :alt: Creating a master subnet example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png + :alt: Creating a master subnet example -On the same page, add a **subnet division rule** that will be used to provision subnets -under the master subnet. +On the same page, add a **subnet division rule**. +This rule defines the criteria for automatically +provisioning subnets under the master subnet. -The type of subnet division rule controls when subnets and IP addresses will be provisioned -for a device. The subnet division rule types currently implemented are described below. +The type of subnet division rule determines when subnets and IP addresses +are assigned to devices. + +The currently supported rule types are described below. + +.. _device_rule: Device Subnet Division Rule -########################### +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This rule type is triggered whenever a device configuration (``config.Config`` model) +This rule triggers when a device configuration (``config.Config`` model) is created for the organization specified in the rule. -Creating a new rule of ^Device^ type will also provision subnets and -IP addresses for existing devices of the organization automatically. +.. note:: + + If a device object is created without any related + configuration object, it will not trigger this rule. -**Note**: a device without a configuration will not trigger this rule. +Creating a new *"Device"* rule will also automatically provision +subnets and IP addresses for existing devices within the organization. + +.. _vpn_rule: VPN Subnet Division Rule -######################## +~~~~~~~~~~~~~~~~~~~~~~~~ -This rule is triggered when a VPN client template is assigned to a device, -provided the VPN server to which the VPN client template relates to has -the same subnet for which the subnet division rule is created. +This rule triggers when a template flagged as *VPN-client* +is assigned to a device configuration, but only if the following +conditions are true: -**Note:** This rule will only work for **WireGuard** and **VXLAN over WireGuard** -VPN servers. +- The VPN server associated with the template + uses the same subnet as the subnet division rule. +- The VPN server is a **WireGuard** or **VXLAN over WireGuard** type. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png - :alt: Creating a subnet division rule example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png + :alt: Creating a subnet division rule example In this example, **VPN subnet division rule** is used. 2. Create a VPN Server -~~~~~~~~~~~~~~~~~~~~~~ +---------------------- Now create a VPN Server and choose the previously created **master subnet** as the subnet for this VPN Server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png - :alt: Creating a VPN Server example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png + :alt: Creating a VPN Server example 3. Create a VPN Client Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- Create a template, setting the **Type** field to **VPN Client** and **VPN** field to use the previously created VPN Server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png - :alt: Creating a VPN Client template example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png + :alt: Creating a VPN Client template example **Note**: You can also check the **Enable by default** field if you want to automatically apply this template to devices that will register in future. 4. Apply VPN Client Template to Devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------- With everything in place, you can now apply the VPN Client Template to devices. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png - :alt: Adding template to device example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png + :alt: Adding template to device example After saving the device, you should see all provisioned Subnets and IPs for this device -under `System Defined Variables <~system-defined-variables>`_. +under :ref:`System Defined Variables *system_defined_variables>`. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png - :alt: Provisioned Subnets and IPs available as System Defined Variables example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png + :alt: Provisioned Subnets and IPs available as System Defined Variables example -Voila! You can now use these variables in configuration of the device. Refer to `How to use configuration variables <~how-to-use-configuration-variables>`_ -section of this documentation to learn how to use configuration variables. +You can now use these :doc:`variables` in the configuration of devices +of your network. Important notes for using Subnet Division -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ -- In the above example Subnet, VPN Server, and VPN Client Template belonged to the **default** organization. - You can use **Systemwide Shared** Subnet, VPN Server, or VPN Client Template too, but - Subnet Division Rule will be always related to an organization. The Subnet Division Rule will only be - triggered when such VPN Client Template will be applied to a Device having the same organization as Subnet Division Rule. +- In the example provided, the Subnet, VPN Server, and VPN Client Template were associated with the **default** organization. + You can also utilize **Systemwide Shared** Subnet, VPN Server, or VPN Client Template; however, remember that + the Subnet Division Rule will always be linked to an organization. It will only be triggered when a VPN Client Template + is applied to a Device with the same organization as the Subnet Division Rule. -- You can also use the configuration variables for provisioned subnets and IPs in the Template. - Each variable will be resolved differently for different devices. E.g. ``OW_subnet1_ip1`` will resolve to - ``10.0.0.1`` for one device and ``10.0.0.55`` for another. Every device gets its own set of subnets and IPs. - But don't forget to provide the default fall back values in the ^default values^ template field - (used mainly for validation). +- Configuration variables can be used for provisioned subnets and IPs in the Template. + Each variable will resolve differently for different devices. For example, ``OW_subnet1_ip1`` will resolve to + ``10.0.0.1`` for one device and ``10.0.0.55`` for another. Every device receives its own set of subnets and IPs. + Ensure to provide default fallback values in the *default values* template field (mainly used for validation). -- The Subnet Division Rule will automatically create a reserved subnet, this subnet can be used - to provision any IP addresses that have to be created manually. The rest of the master subnet - address space **must not** be interfered with or the automation implemented in this module - will not work. +- The Subnet Division Rule automatically creates a reserved subnet, which can be utilized + to provision any IP addresses that need to be created manually. The remaining address space of the master subnet + must not be interfered with, or the automation implemented in this module will not function. -- The above example used `VPN subnet division rule <~vpn-subnet-division-rule>`_. Similarly, - `device subnet division rule <~device-subnet-division-rule>`_ can be used, which only requires - `creating a subnet and a subnet division rule <~1-create-a-subnet-and-a-subnet-division-rule>`_. +- The example provided used the :ref:`VPN subnet division rule `. Similarly, + the :ref:`device subnet division rule ` can be employed, requiring only + :ref:`the creation of a subnet and a subnet division rule `. -Limitations of Subnet Division -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Limitations of Subnet Division Rules +------------------------------------ -In the current implementation, it is not possible to change ^Size^, ^Number of Subnets^ and -^Number of IPs^ fields of an existing subnet division rule due to following reasons: +In the current implementation, it is not possible to change *Size*, *Number of Subnets* and +*Number of IPs* fields of an existing subnet division rule due to following reasons: Size -#### +~~~~ Allowing to change size of provisioned subnets of an existing subnet division rule will require rebuilding of Subnets and IP addresses which has possibility of breaking existing configurations. Number of Subnets -################# +~~~~~~~~~~~~~~~~~ Allowing to decrease number of subnets of an existing subnet division rule can create patches of unused subnets dispersed everywhere in the master subnet. @@ -129,13 +151,25 @@ Allowing to increase number of subnets will break the continuous allocation of s every device. It can also break configuration of devices. Number of IPs -############# +~~~~~~~~~~~~~ + +**Decreasing the number of IPs** in an existing subnet division rule +is not allowed as it can lead to deletion of IP addresses, potentially +breaking configurations of existing devices. + +**Increasing the number of IPs is allowed**. + +If you need to modify any of these fields +(**Size**, **Number of Subnets**, or **Number of IPs**), we recommend +to proceed as follows: + +1. Delete the existing rule. +2. Create a new rule. -Allowing to decrease number of IPs of an existing subnet division rule -will lead to deletion of IP Addresses which can break configuration of devices being used. -It **is allowed** to increase number of IPs. +The automation will provision new subnets and addresses according to +the new parameters to any existing devices that are eligible to the +subnet division rule. -If you want to make changes to any of above fields, delete the existing rule and create a -new one. The automation will provision for all existing devices that meets the criteria -for provisioning. **WARNING**: It is possible that devices get different subnets and IPs -from previous provisioning. +However, be aware that existing devices **will probably +be reassigned different subnets and IP addresses** than the ones +used previously. From 659e6b105fb8b6db1654d251c7b155e843143242 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 16 May 2024 19:21:08 -0400 Subject: [PATCH 15/44] [docs] Fixed more links --- docs/developer/extending.rst | 51 +++---- docs/developer/utils.rst | 16 +-- docs/user/intro.rst | 2 +- docs/user/rest-api.rst | 213 +++++++++++++++------------- docs/user/settings.rst | 50 ++++--- docs/user/shell-commands.rst | 45 +++--- docs/user/subnet-division-rules.rst | 18 ++- 7 files changed, 219 insertions(+), 176 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 2be9524ff..8ad7719b2 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,5 +1,5 @@ Extending openwisp-controller ------------------------------ +============================= .. include:: ../partials/developer-docs.rst @@ -27,7 +27,7 @@ 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 project & custom apps -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------- Firstly, to get started you need to create a django project:: @@ -60,14 +60,14 @@ For more information about how to work with django projects and django apps, please refer to the `django documentation `_. 2. Install ``openwisp-controller`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------- Install (and add to the requirement of your project) openwisp-controller:: pip install openwisp-controller 3. Add your apps in INSTALLED_APPS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------- Now you need to add ``mycontroller.sample_config``, ``mycontroller.sample_pki``, ``mycontroller.sample_connection``, @@ -120,7 +120,7 @@ Substitute ``mycontroller``, ``sample_config``, ``sample_pki``, ``sample_connect ``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. 4. Add ``EXTENDED_APPS`` -~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ Add the following to your ``settings.py``: @@ -137,7 +137,7 @@ Add the following to your ``settings.py``: ) 5. Add ``openwisp_utils.staticfiles.DependencyFinder`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------------ Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` in your ``settings.py``: @@ -151,7 +151,7 @@ Add ``openwisp_utils.staticfiles.DependencyFinder`` to ] 6. Add ``openwisp_utils.loaders.DependencyLoader`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------- Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``, but ensure it comes before @@ -181,7 +181,7 @@ in your ``settings.py``, but ensure it comes before ] 5. Initial Database setup -~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------- Ensure you are using one of the available geodjango backends, eg: @@ -197,7 +197,7 @@ Ensure you are using one of the available geodjango backends, eg: For more information about GeoDjango, please refer to the `geodjango documentation `_. 6. Django Channels Setup -~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ Create ``asgi.py`` in your project folder and add following lines in it: @@ -221,7 +221,7 @@ Create ``asgi.py`` in your project folder and add following lines in it: ) 7. Other Settings -~~~~~~~~~~~~~~~~~ +----------------- Add the following settings to ``settings.py``: @@ -244,7 +244,7 @@ For more information about CHANNEL_LAYERS setting, please refer to the `CHANNEL_LAYERS documentation `_. 6. Inherit the AppConfig class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------ Please refer to the following files in the sample app of the test project: @@ -274,7 +274,7 @@ For more information regarding the concept of ``AppConfig`` please refer to the `"Applications" section in the django documentation `_. 7. Create your custom models -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------- For the purpose of showing an example, we added a simple "details" field to the models of the sample app in the test project. @@ -291,7 +291,7 @@ You can add fields in a similar way in your ``models.py`` file. the `"Models" section in the django documentation `_. 8. Add swapper configurations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- Once you have created the models, add the following to your ``settings.py``: @@ -323,7 +323,7 @@ Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. 9. Create database migrations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- Create database migrations:: @@ -347,7 +347,7 @@ For more information, refer to the `"Migrations" section in the django documentation `_. 10. Create the admin -~~~~~~~~~~~~~~~~~~~~ +-------------------- Refer to the ``admin.py`` file of the sample app. @@ -567,7 +567,7 @@ sample_subnet_division # add your changes here 11. Create root URL configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- .. code-block:: python @@ -594,7 +594,7 @@ For more information about URL configuration in django, please refer to the `"URL dispatcher" section in the django documentation `_. 12. 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 @@ -628,7 +628,7 @@ For more information about automated tests in django, please refer to `"Testing in Django" `_. Other base classes that can be inherited and extended -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------------- The following steps are not required and are intended for more advanced customization. @@ -650,10 +650,13 @@ Remember to change ``geo_views`` location in ``urls.py`` in point 11 for extendi For more information about django views, please refer to the `views section in the django documentation `_. +.. _custom_subnet_division_rule_types: + Custom Subnet Division Rule Types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- -It is possible to create your own `subnet division rule types <#subnet-division-app>`_. +It is possible to create your own +:doc:`subnet division rule types <../user/subnet-division-rules>`. The rule type determines when subnets and IPs will be provisioned and when they will be destroyed. @@ -722,7 +725,7 @@ for that device. cls.provision_receiver(device, created=True) After creating a class for your custom rule type, you will need to set -`OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES <#openwisp-controller-subnet-division-types>`_ +:ref:`OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES ` setting as follows: .. code-block:: python @@ -733,7 +736,7 @@ setting as follows: ('mycontroller.sample_subnet_division.rules_types.custom.CustomRuleType', 'Custom Rule'), ) -Registering new notification types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +More Utilities to Extend OpenWISP Controller +-------------------------------------------- -Refer to ... +See :doc:`utils`. diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index b1d7d4f3e..070e67f7c 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -221,7 +221,7 @@ It is not emitted when the device is created. The signal is emitted when subnets and IP addresses have been provisioned for a ``VpnClient`` for a VPN server with a subnet with -`subnet division rule <#subnet-division-app>`_. +:doc:`subnet division rule <../user/subnet-division-rules>`. ``vpn_server_modified`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -278,8 +278,8 @@ or unregister existing commands from your code. +--------------------+------------------------------------------------------------------+ | ``command_name`` | A ``str`` defining identifier for the command. | +--------------------+------------------------------------------------------------------+ -| ``command_config`` | A ``dict`` defining configuration of the command | -| | as shown in `^Command Configuration^ <~command-configuration>`_. | +| ``command_config`` | A ``dict`` like the one shown in | +| | :ref:`Command Configuration: schema `. | +--------------------+------------------------------------------------------------------+ **Note:** It will raise ``ImproperlyConfigured`` exception if a command is already @@ -316,11 +316,11 @@ Registering notification types You can define your own notification types using ``register_notification_type`` function from OpenWISP Notifications. -For more information, see the relevant :ref:`documentation section about -registering notification types in 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 -`"notify" signal provided in openwisp-notifications -`_ +:doc:`"notify" signal provided the Notifications module +` to send notifications for this type. diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 34e46b233..947c32422 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -67,7 +67,7 @@ SSH The SSH connector allows the controller to initialize connections to the devices in order to perform -:doc:`push operations `, e.g.: +:doc:`push operations `, e.g.: - Sending configuration updates. - :doc:`Executing shell commands `. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 0b4e1322f..2622ecf54 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -1,35 +1,40 @@ REST API Reference ------------------- +================== + +.. _controller_live_documentation: Live documentation -~~~~~~~~~~~~~~~~~~ +------------------ .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.png A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. +.. _controller_browsable_web_interface: + Browsable web interface -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.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. Authentication -~~~~~~~~~~~~~~ +-------------- -See openwisp-users: `authenticating with the user token -`_. +See :ref:`authenticating_rest_api`. -When browsing the API via the `Live documentation <#live-documentation>`_ -or the `Browsable web page <#browsable-web-interface>`_, you can also use +When browsing the API via the :ref:`controller_live_documentation` +or the :ref:`controller_browsable_web_interface`, you can also use the session authentication by logging in the django admin. Pagination -~~~~~~~~~~ +---------- All *list* endpoints support the ``page_size`` parameter that allows paginating the results in conjunction with the ``page`` parameter. @@ -39,16 +44,22 @@ the results in conjunction with the ``page`` parameter. GET /api/v1/controller/template/?page_size=10 GET /api/v1/controller/template/?page_size=10&page=2 +.. _controller_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, +Since the detailed explanation is contained in the +:ref:`controller_live_documentation` +and in the +:ref:`controller_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. +for further information please open the URL of the endpoint +in your browser. List devices -############ +~~~~~~~~~~~~ .. code-block:: text @@ -116,24 +127,22 @@ their creation time using the ``creation_time``. # Created is less than GET /api/v1/controller/device/?created__lt={creation_time} - - Create device -############# +~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/device/ Get device detail -################# +~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/ Download device configuration -############################# +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -142,14 +151,14 @@ Download device configuration The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific device. Change details of device -######################## +~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/device/{id}/ Patch details of device -####################### +~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -210,119 +219,119 @@ to the ``config`` and cannot be removed. }' Delete device -############# +~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/device/{id}/ List device connections -####################### +~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/connection/ Create device connection -######################## +~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/device/{id}/connection/ Get device connection detail -############################ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/connection/{id}/ Change device connection detail -############################### +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/device/{id}/connection/{id}/ Patch device connection detail -############################## +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/device/{id}/connection/{id}/ Delete device connection -######################## +~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/device/{id}/connection/{id}/ List credentials -################ +~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/connection/credential/ Create credential -################# +~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/connection/credential/ Get credential detail -##################### +~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/connection/credential/{id}/ Change credential detail -######################## +~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/connection/credential/{id}/ Patch credential detail -####################### +~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/connection/credential/{id}/ Delete credential -################# +~~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/connection/credential/{id}/ List commands of a device -######################### +~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/command/ Execute a command a device -########################## +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/device/{id}/command/ Get command details -################### +~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{device_id}/command/{command_id}/ List device groups -################## +~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -350,14 +359,14 @@ device object using the ``empty`` (eg. true or false). Create device group -################### +~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/group/ Get device group detail -####################### +~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -366,16 +375,17 @@ Get device group detail .. _change_device_group_detail: Change device group detail -########################## +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/group/{id}/ -This endpoint allows to change the `group templates <#group-templates>`_ too. +This endpoint allows to change the +:ref:`device_group_templates` too. Get device group from certificate common name -############################################# +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -393,16 +403,17 @@ of certificate's organization as show in the example below: GET /api/v1/controller/cert/{common_name}/group/?org={org1_slug},{org2_slug} Get device location -################### +~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/location/ +.. _create_device_location: Create device location -###################### +~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -549,25 +560,27 @@ floorplan for that location using this endpoint. -F 'floorplan.image=@floorplan.png' Change details of device location -################################# +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/device/{id}/location/ **Note:** This endpoint can be used to update related ``Location`` -and ``Floorplan`` objects. Refer `examples of "Create device location" -section for information on payload format <#create-device-location>`_. +and ``Floorplan`` objects. Refer to the +:ref:`examples in the "Create device location" +section ` +for information on payload format. Delete device location -###################### +~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/device/{id}/location/ Get device coordinates -###################### +~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -585,7 +598,7 @@ assumes that the device is updating it's position. 'http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/coordinates/?key=10a0cb5a553c71099c0e4ef236435496' Update device coordinates -######################### +~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -621,7 +634,7 @@ assumes that the device is updating it's position. }' List locations -############## +~~~~~~~~~~~~~~ .. code-block:: text @@ -641,7 +654,7 @@ to get list locations that belongs to an organization. GET /api/v1/controller/location/?organization_slug={organization_slug} Create location -############### +~~~~~~~~~~~~~~~ .. code-block:: text @@ -700,14 +713,14 @@ format, like following: } Get location details -#################### +~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/location/{pk}/ Change location details -####################### +~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -717,25 +730,27 @@ Change location details edited or changed. Setting the ``type`` of location to outdoor will remove all the floorplans associated with it. -Refer `examples of "Create location" -section for information on payload format <#create-location>`_. +Refer to the +:ref:`examples in the "Create device location" +section ` +for information on payload format. Delete location -############### +~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/location/{pk}/ List devices in a location -########################## +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/location/{id}/device/ List locations with devices deployed (in GeoJSON format) -######################################################## +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Note**: this endpoint will only list locations that have been assigned to a device. @@ -757,7 +772,7 @@ to get list location of devices from that organization. GET /api/v1/controller/location/geojson/?organization_slug={organization_slug} List floorplans -############### +~~~~~~~~~~~~~~~ .. code-block:: text @@ -777,35 +792,35 @@ to get list floorplans that belongs to an organization. GET /api/v1/controller/floorplan/?organization_slug={organization_slug} Create floorplan -################ +~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/floorplan/ Get floorplan details -##################### +~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/floorplan/{pk}/ Change floorplan details -######################## +~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/floorplan/{pk}/ Delete floorplan -################ +~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/floorplan/{pk}/ List templates -############## +~~~~~~~~~~~~~~ .. code-block:: text @@ -870,21 +885,21 @@ their creation time using the ``creation_time``. GET /api/v1/controller/template/?created__lt={creation_time} Create template -############### +~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/template/ Get template detail -################### +~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/template/{id}/ Download template configuration -############################### +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -894,28 +909,28 @@ The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific template. Change details of template -########################## +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/template/{id}/ Patch details of template -######################### +~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/template/{id}/ Delete template -############### +~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/template/{id}/ List VPNs -######### +~~~~~~~~~ .. code-block:: text @@ -950,21 +965,21 @@ using the ``organization_id`` or ``organization_slug``. GET /api/v1/controller/vpn/?organization_slug={organization_slug} Create VPN -########## +~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/vpn/ Get VPN detail -############## +~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/vpn/{id}/ Download VPN configuration -########################## +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -974,42 +989,42 @@ The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific VPN. Change details of VPN -##################### +~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/vpn/{id}/ Patch details of VPN -#################### +~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/vpn/{id}/ Delete VPN -########## +~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/vpn/{id}/ List CA -####### +~~~~~~~ .. code-block:: text GET /api/v1/controller/ca/ Create new CA -############# +~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/ca/ Import existing CA -################## +~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1020,28 +1035,28 @@ and ``private_key`` fields have to be filled in the ``HTML`` form or included in the ``JSON`` format. Get CA Detail -############# +~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/ca/{id}/ Change details of CA -#################### +~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/ca/{id}/ Patch details of CA -################### +~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/ca/{id}/ Download CA(crl) -################ +~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1051,35 +1066,35 @@ The above endpoint triggers the download of ``{id}.crl`` file containing up to date CRL of that specific CA. Delete CA -######### +~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/ca/{id}/ Renew CA -######## +~~~~~~~~ .. code-block:: text POST /api/v1/controller/ca/{id}/renew/ List Cert -######### +~~~~~~~~~ .. code-block:: text GET /api/v1/controller/cert/ Create new Cert -############### +~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/cert/ Import existing Cert -#################### +~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1090,42 +1105,42 @@ Import existing Cert in the ``HTML`` form or included in the ``JSON`` format. Get Cert Detail -############### +~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/cert/{id}/ Change details of Cert -###################### +~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/cert/{id}/ Patch details of Cert -##################### +~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/cert/{id}/ Delete Cert -########### +~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/cert/{id}/ Renew Cert -########## +~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/cert/{id}/renew/ Revoke Cert -########### +~~~~~~~~~~~ .. code-block:: text diff --git a/docs/user/settings.rst b/docs/user/settings.rst index ffa9469e6..a651d141e 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -259,7 +259,7 @@ Additional context that is passed to the default context of each device object. ``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide configuration variables. For more information regarding how to use configuration variables in OpenWISP, -see `How to use configuration variables <#how-to-use-configuration-variables>`_. +refer to :doc:`variables`. For technical information about how variables are handled in the lower levels of OpenWISP, see `netjsonconfig context: configuration variables @@ -460,8 +460,12 @@ For example, if we want to change the verbose name to "Hotspot", we could write: | **default**: | ``False`` | +--------------+-----------+ -Setting this to ``True`` will hide subnets and IPs generated using `subnet division rules <#subnet-division-app>`_ -from being displayed on the changelist view of Subnet and IP admin. +Setting this to ``True`` will hide subnets and IP addresses generated +by :doc:`subnet division rules ` +from being displayed in the list of Subnets and IP addresses in the +admin dashboard. + +.. _OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES: ``OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -478,9 +482,11 @@ from being displayed on the changelist view of Subnet and IP admin. | | | +--------------+---------------------------------------------------------------------------------------------------------+ -`Available types for Subject Division Rule <#device-subnet-division-rule>`_ objects. -For more information on how to write your own types, read -`"Custom Subnet Division Rule Types" section of this documentation <#custom-subnet-division-rule-types>`_ +Available types for +:doc:`Subject Division Rule ` objects. + +For more information on how to write your own types, please refer to: +:ref:`custom_subnet_division_rule_types`. ``OPENWISP_CONTROLLER_API`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -492,7 +498,8 @@ For more information on how to write your own types, read +--------------+-----------+ Indicates whether the API for Openwisp Controller is enabled or not. -To disable the API by default add `OPENWISP_CONTROLLER_API = False` in `settings.py` file. +To disable the API by default add ``OPENWISP_CONTROLLER_API = False`` +in your project ``settings.py`` file. ``OPENWISP_CONTROLLER_API_HOST`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -516,8 +523,8 @@ Allows to specify backend URL for API requests, if the frontend is hosted separa | **default**: | ``[]`` | +--------------+----------+ -Allows to specify a ``list`` of tuples for adding commands as described in -`'How to define custom commands" <#how-to-define-new-options-in-the-commands-menu>`_ section. +Allows to specify a ``list`` of tuples for adding commands +as described in the section: :ref:`defining_new_menu_options`. ``OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -562,7 +569,8 @@ while all the other organizations will have all command types enabled. | **default**: | ``{'type': 'object', 'properties': {}}`` | +--------------+------------------------------------------+ -Allows specifying JSONSchema used for validating meta-data of `Device Group <#device-groups>`__. +Allows specifying JSONSchema used for +validating the meta-data of :doc:`device-groups`. ``OPENWISP_CONTROLLER_SHARED_MANAGEMENT_IP_ADDRESS_SPACE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -617,7 +625,7 @@ for DSA interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by reading the ``os`` field of the ``Device`` object. However, if the firmware you are using has a custom firmware identifier, the system will not be able to figure out whether it should use the new syntax and it will default to -`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK <#>`_. +:ref:`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK `. If you want to make sure the system can parse your custom firmware identifier properly, you can follow the example below. @@ -644,6 +652,8 @@ Example: **Note**: The OS identifier should be a regular expression as shown in above example. +.. _OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK: + ``OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -696,12 +706,18 @@ while emtpy groups do not have any device assigned. This setting is utilized by background API tasks executed -by `ZeroTier VPN servers and ZeroTier VPN clients <#how-to-setup-zerotier-tunnels>`_ to handle recoverable -HTTP status codes such as 429, 500, 502, 503, and 504. These tasks are retried with a maximum -of 5 attempts with an exponential backoff and jitter, with a maximum delay of 10 minutes. +by :doc:`ZeroTier VPN servers and ZeroTier VPN clients ` +to handle recoverable HTTP status codes such as 429, 500, 502, 503, +and 504. + +These tasks are retried with a maximum of 5 attempts with an +exponential backoff and jitter, with a maximum delay of 10 minutes. This feature ensures that ZeroTier Service API calls -are resilient to recoverable failures, improving the reliability of the system. +are resilient to recoverable failures, improving the +reliability of the system. -For more information on these settings, you can refer to the `the celery documentation regarding automatic retries -for known errors. `_ +For more information on these settings, you can refer to the +`the celery documentation regarding automatic retries +for known errors. +`_ diff --git a/docs/user/shell-commands.rst b/docs/user/shell-commands.rst index b8798eaae..79f1aa7da 100644 --- a/docs/user/shell-commands.rst +++ b/docs/user/shell-commands.rst @@ -1,5 +1,5 @@ Sending Commands to Devices ---------------------------- +=========================== By default, there are three options in the **Send Command** dropdown: @@ -16,7 +16,7 @@ allows you to execute any command on the device as shown in the example below. **Note**: in order for this feature to work, a device needs to have at least one **Access Credential** (see -:doc:`How to configure push updates `). +:doc:`How to configure push updates `). The **Send Command** button will be hidden until the device has at least one **Access Credential**. @@ -25,13 +25,16 @@ If you need to allow your users to quickly send specific commands that are used network regardless of your users' knowledge of Linux shell commands, you can add new commands by following instructions in the :ref:`defining_new_menu_options` section below. -If you are an advanced user and want to register commands programatically, then refer to -`^Register / Unregistering commands^ <~registering--unregistering-commands>`_ section. +.. note:: + + If you're an advanced user and want to learn how to register + commands programmatically, refer to the + :ref:`registering_unregistering_commands` section. .. _defining_new_menu_options: -Defining new options in the commands menu -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Defining New Options in the Commands Menu +----------------------------------------- Let's explore to define new custom commands to help users perform additional management actions @@ -44,7 +47,7 @@ The following example defines a simple command that can ``ping`` an input .. code-block:: python - ~ In yourproject/settings.py + # In yourproject/settings.py def ping_command_callable(destination_address, interface_name=None): command = f'ping -c 4 {destination_address}' @@ -79,20 +82,22 @@ The following example defines a simple command that can ``ping`` an input ) ] -The above code will add the ^Ping^ command in the user interface as show +The above code will add the *Ping* command in the user interface as show in the GIF below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/ping_command_example.gif :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/ping_command_example.gif - :alt: Adding a ^ping^ command + :alt: Adding a *ping* command The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of ``tuple`` each containing two elements. The first element of the tuple should contain an identifier for the command and the second element should contain a ``dict`` defining configuration of the command. +.. _comand_configuration: + Command Configuration -##################### +~~~~~~~~~~~~~~~~~~~~~ The ``dict`` defining configuration for command should contain following keys: @@ -113,19 +118,19 @@ Here is a detailed explanation of the schema used in above example: .. code-block:: python { - ~ Name of the command displayed in ^Send Command^ widget + # Name of the command displayed in *Send Command* widget 'title': 'Ping', - ~ Use type ^object^ if the command needs to accept inputs - ~ Use type ^null^ if the command does not accepts any input + # Use type *object* if the command needs to accept inputs + # Use type *null* if the command does not accepts any input 'type': 'object', - ~ Specify list of inputs that are required + # Specify list of inputs that are required 'required': ['destination_address'], - ~ Define the inputs for the commands along with their properties + # Define the inputs for the commands along with their properties 'properties': { 'destination_address': { - ~ type of the input value + # type of the input value 'type': 'string', - ~ label used for displaying this input field + # label used for displaying this input field 'title': 'Destination Address', }, 'interface_name': { @@ -133,9 +138,9 @@ Here is a detailed explanation of the schema used in above example: 'title': 'Interface Name', }, }, - ~ Error message to be shown if validation fails + # Error message to be shown if validation fails 'message': 'Destination Address cannot be empty'), - ~ Whether specifying addtionaly inputs is allowed from the input form + # Whether specifying addtionaly inputs is allowed from the input form 'additionalProperties': False, } @@ -153,7 +158,7 @@ The example above includes a callable(``ping_command_callable``) for ``ping`` command. How to register or unregister commands -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------- Refer to :ref:`registering_unregistering_commands` diff --git a/docs/user/subnet-division-rules.rst b/docs/user/subnet-division-rules.rst index 162b70798..13d6b5432 100644 --- a/docs/user/subnet-division-rules.rst +++ b/docs/user/subnet-division-rules.rst @@ -31,6 +31,12 @@ are assigned to devices. The currently supported rule types are described below. +.. note:: + + For information on how to write your own subnet division rule types, + please refer to: + :ref:`custom_subnet_division_rule_types`. + .. _device_rule: Device Subnet Division Rule @@ -53,12 +59,10 @@ VPN Subnet Division Rule ~~~~~~~~~~~~~~~~~~~~~~~~ This rule triggers when a template flagged as *VPN-client* -is assigned to a device configuration, but only if the following -conditions are true: - -- The VPN server associated with the template - uses the same subnet as the subnet division rule. -- The VPN server is a **WireGuard** or **VXLAN over WireGuard** type. +is assigned to a device configuration, but only if the +VPN server associated with the VPN-client template +uses the same subnet to which the subnet division rule +is assignated to. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png @@ -99,7 +103,7 @@ With everything in place, you can now apply the VPN Client Template to devices. :alt: Adding template to device example After saving the device, you should see all provisioned Subnets and IPs for this device -under :ref:`System Defined Variables *system_defined_variables>`. +under :ref:`System Defined Variables `. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png From 86fd27db4221724302ca09974a0745b031de0e3b Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Fri, 17 May 2024 20:06:43 -0400 Subject: [PATCH 16/44] [docs] Reformatted docs with docstrfmt tool --- docs/developer/extending.rst | 595 ++++++++++-------- docs/developer/index.rst | 12 +- docs/developer/installation.rst | 39 +- docs/developer/utils.rst | 180 +++--- docs/index.rst | 50 +- docs/partials/developer-docs.rst | 13 +- docs/partials/shared-object.rst | 10 +- docs/user/device-groups.rst | 122 ++-- docs/user/import-export.rst | 26 +- docs/user/intro.rst | 116 ++-- docs/user/organization-limits.rst | 6 +- docs/user/push-operations.rst | 98 ++- docs/user/rest-api.rst | 312 +++++----- docs/user/settings.rst | 901 ++++++++++++++-------------- docs/user/shell-commands.rst | 143 +++-- docs/user/subnet-division-rules.rst | 152 +++-- docs/user/templates.rst | 186 +++--- docs/user/variables.rst | 97 ++- docs/user/vxlan-wireguard.rst | 97 ++- docs/user/wireguard.rst | 125 ++-- docs/user/zerotier.rst | 122 ++-- 21 files changed, 1681 insertions(+), 1721 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 8ad7719b2..42ef6e59a 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -3,48 +3,52 @@ Extending openwisp-controller .. include:: ../partials/developer-docs.rst -One of the core values of the OpenWISP project is -`Software Reusability `_, -for this reason *openwisp-controller* 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 `Software Reusability +`_, +for this reason *openwisp-controller* 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-controller*, -you need to perform the steps described in this section. +In order to implement your custom version of *openwisp-controller*, you need to perform +the steps described in this section. -When in doubt, the code in the -`test project `_ -will serve you as source of truth: just replicate and adapt that code -to get a basic derivative of *openwisp-controller* working. +When in doubt, the code in the `test project +`_ will +serve you as source of truth: just replicate and adapt that code to get a basic +derivative of *openwisp-controller* working. If you want to add new users fields, please follow the `tutorial to extend the -openwisp-users `_. -As an example, we have extended *openwisp-users* to *sample_users* app and -added a field ``social_security_number`` in the `sample_users/models.py +openwisp-users `_. As +an example, we have extended *openwisp-users* to *sample_users* app and added a field +``social_security_number`` in the `sample_users/models.py `_. -**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. +**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 project & custom apps ---------------------------------------- -Firstly, to get started you need to create a django project:: +Firstly, to get started you need to create a django project: + +.. code-block:: django-admin startproject mycontroller -Now, you need to do is to create some new django apps which will -contain your custom version of *openwisp-controller*. +Now, you need to do is to create some new django apps which will contain your custom +version of *openwisp-controller*. A django project is a collection of django apps. There are 4 django apps in the -openwisp_controller project, namely config, pki, connection & geo. -You'll need to create 4 apps in your project for each app in openwisp_controller. +openwisp_controller project, namely config, pki, connection & geo. You'll need to create +4 apps in your project for each app in openwisp_controller. + +A django app is nothing more than a `python package +`_ (a directory of python +scripts), in the following examples we'll call these django app ``sample_config``, +``sample_pki``, ``sample_connection``, ``sample_geo`` & ``sample_subnet_division``. 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 app -``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` -& ``sample_subnet_division``. but you can name it how you want:: +.. code-block:: django-admin startapp sample_config django-admin startapp sample_pki @@ -52,28 +56,31 @@ A django app is nothing more than a django-admin startapp sample_geo django-admin startapp sample_subnet_division -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. -For more information about how to work with django projects and django apps, -please refer to the `django documentation `_. +For more information about how to work with django projects and django apps, please +refer to the `django documentation +`_. 2. Install ``openwisp-controller`` ---------------------------------- -Install (and add to the requirement of your project) openwisp-controller:: +Install (and add to the requirement of your project) openwisp-controller: + +.. code-block:: pip install openwisp-controller 3. Add your apps in INSTALLED_APPS ---------------------------------- -Now you need to add ``mycontroller.sample_config``, -``mycontroller.sample_pki``, ``mycontroller.sample_connection``, -``mycontroller.sample_geo`` & ``mycontroller.sample_subnet_division`` to -``INSTALLED_APPS`` in your ``settings.py``, ensuring also that -``openwisp_controller.config``, ``openwisp_controller.geo``, +Now you need to add ``mycontroller.sample_config``, ``mycontroller.sample_pki``, +``mycontroller.sample_connection``, ``mycontroller.sample_geo`` & +``mycontroller.sample_subnet_division`` to ``INSTALLED_APPS`` in your ``settings.py``, +ensuring also that ``openwisp_controller.config``, ``openwisp_controller.geo``, ``openwisp_controller.pki``, ``openwisp_controller.connnection`` & ``openwisp_controller.subnet_division`` have been removed: @@ -82,38 +89,38 @@ Now you need to add ``mycontroller.sample_config``, # Remember: Order in INSTALLED_APPS is important. INSTALLED_APPS = [ # other django installed apps - 'openwisp_utils.admin_theme', - 'admin_auto_filters', + "openwisp_utils.admin_theme", + "admin_auto_filters", # all-auth - 'django.contrib.sites', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', + "django.contrib.sites", + "allauth", + "allauth.account", + "allauth.socialaccount", # openwisp2 module # 'openwisp_controller.config', <-- comment out or delete this line # 'openwisp_controller.pki', <-- comment out or delete this line # 'openwisp_controller.geo', <-- comment out or delete this line # 'openwisp_controller.connection', <-- comment out or delete this line # 'openwisp_controller.subnet_division', <-- comment out or delete this line - 'mycontroller.sample_config', - 'mycontroller.sample_pki', - 'mycontroller.sample_geo', - 'mycontroller.sample_connection', - 'mycontroller.sample_subnet_division', - 'openwisp_users', + "mycontroller.sample_config", + "mycontroller.sample_pki", + "mycontroller.sample_geo", + "mycontroller.sample_connection", + "mycontroller.sample_subnet_division", + "openwisp_users", # admin - 'django.contrib.admin', + "django.contrib.admin", # other dependencies - 'sortedm2m', - 'reversion', - 'leaflet', + "sortedm2m", + "reversion", + "leaflet", # rest framework - 'rest_framework', - 'rest_framework_gis', + "rest_framework", + "rest_framework_gis", # channels - 'channels', + "channels", # django-import-export - 'import_export', + "import_export", ] Substitute ``mycontroller``, ``sample_config``, ``sample_pki``, ``sample_connection``, @@ -127,54 +134,54 @@ Add the following to your ``settings.py``: .. code-block:: python EXTENDED_APPS = ( - 'django_x509', - 'django_loci', - 'openwisp_controller.config', - 'openwisp_controller.pki', - 'openwisp_controller.geo', - 'openwisp_controller.connection', - 'openwisp_controller.subnet_division', + "django_x509", + "django_loci", + "openwisp_controller.config", + "openwisp_controller.pki", + "openwisp_controller.geo", + "openwisp_controller.connection", + "openwisp_controller.subnet_division", ) 5. Add ``openwisp_utils.staticfiles.DependencyFinder`` ------------------------------------------------------ -Add ``openwisp_utils.staticfiles.DependencyFinder`` to -``STATICFILES_FINDERS`` in your ``settings.py``: +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', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "openwisp_utils.staticfiles.DependencyFinder", ] 6. Add ``openwisp_utils.loaders.DependencyLoader`` -------------------------------------------------- -Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` -in your ``settings.py``, but ensure it comes before +Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your +``settings.py``, but ensure it comes before ``django.template.loaders.app_directories.Loader``: .. code-block:: python TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'OPTIONS': { - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'openwisp_utils.loaders.DependencyLoader', - 'django.template.loaders.app_directories.Loader', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "OPTIONS": { + "loaders": [ + "django.template.loaders.filesystem.Loader", + "openwisp_utils.loaders.DependencyLoader", + "django.template.loaders.app_directories.Loader", ], - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'openwisp_utils.admin_theme.context_processor.menu_items', - 'openwisp_notifications.context_processors.notification_api_settings', + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "openwisp_utils.admin_theme.context_processor.menu_items", + "openwisp_notifications.context_processors.notification_api_settings", ], }, } @@ -188,13 +195,14 @@ Ensure you are using one of the available geodjango backends, eg: .. code-block:: python DATABASES = { - 'default': { - 'ENGINE': 'openwisp_utils.db.backends.spatialite', - 'NAME': 'openwisp-controller.db', + "default": { + "ENGINE": "openwisp_utils.db.backends.spatialite", + "NAME": "openwisp-controller.db", } } -For more information about GeoDjango, please refer to the `geodjango documentation `_. +For more information about GeoDjango, please refer to the `geodjango documentation +`_. 6. Django Channels Setup ------------------------ @@ -209,14 +217,16 @@ Create ``asgi.py`` in your project folder and add following lines in it: from django.core.asgi import get_asgi_application from openwisp_controller.routing import get_routes + # You can also add your routes like this from my_app.routing import my_routes application = ProtocolTypeRouter( - { "http": get_asgi_application(), - 'websocket': AllowedHostsOriginValidator( + { + "http": get_asgi_application(), + "websocket": AllowedHostsOriginValidator( AuthMiddlewareStack(URLRouter(get_routes() + my_routes)) - ) + ), } ) @@ -227,21 +237,21 @@ Add the following settings to ``settings.py``: .. code-block:: python - FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + FORM_RENDERER = "django.forms.renderers.TemplatesSetting" - ASGI_APPLICATION = 'my_project.asgi.application' + ASGI_APPLICATION = "my_project.asgi.application" CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer' - }, + "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}, } -For more information about FORM_RENDERER setting, please refer to the -`FORM_RENDERER documentation `_. -For more information about ASGI_APPLICATION setting, please refer to the -`ASGI_APPLICATION documentation `_. -For more information about CHANNEL_LAYERS setting, please refer to the -`CHANNEL_LAYERS documentation `_. +For more information about FORM_RENDERER setting, please refer to the `FORM_RENDERER +documentation `_. For +more information about ASGI_APPLICATION setting, please refer to the `ASGI_APPLICATION +documentation +`_. +For more information about CHANNEL_LAYERS setting, please refer to the `CHANNEL_LAYERS +documentation +`_. 6. Inherit the AppConfig class ------------------------------ @@ -249,46 +259,59 @@ For more information about CHANNEL_LAYERS setting, please refer to the Please refer to the following files in the sample app of the test project: - sample_config: - - `sample_config/__init__.py `_. - - `sample_config/apps.py `_. - + - `sample_config/__init__.py + `_. + - `sample_config/apps.py + `_. - sample_geo: - - `sample_geo/__init__.py `_. - - `sample_geo/apps.py `_. - + - `sample_geo/__init__.py + `_. + - `sample_geo/apps.py + `_. - sample_pki: - - `sample_pki/__init__.py `_. - - `sample_pki/apps.py `_. - + - `sample_pki/__init__.py + `_. + - `sample_pki/apps.py + `_. - sample_connection: - - `sample_connection/__init__.py `_. - - `sample_connection/apps.py `_. - + - `sample_connection/__init__.py + `_. + - `sample_connection/apps.py + `_. - sample_subnet_division: - - `sample_subnet_division/__init__.py `_. - - `sample_subnet_division/apps.py `_. + - `sample_subnet_division/__init__.py + `_. + - `sample_subnet_division/apps.py + `_. You have to replicate and adapt that code in your project. -For more information regarding the concept of ``AppConfig`` please refer to -the `"Applications" section in the django documentation `_. +For more information regarding the concept of ``AppConfig`` please refer to the +`"Applications" section in the django documentation +`_. 7. Create your custom models ---------------------------- -For the purpose of showing an example, we added a simple "details" field -to the models of the sample app in the test project. +For the purpose of showing an example, we added a simple "details" field to the models +of the sample app in the test project. -- `sample_config models `_ -- `sample_geo models `_ -- `sample_pki models `_ -- `sample_connection models `_ -- `sample_subnet_division `_ +- `sample_config models + `_ +- `sample_geo models + `_ +- `sample_pki models + `_ +- `sample_connection models + `_ +- `sample_subnet_division + `_ You can add fields in a similar way in your ``models.py`` file. -**Note**: for doubts regarding how to use, extend or develop models please refer to -the `"Models" section in the django documentation `_. +**Note**: for doubts regarding how to use, extend or develop models please refer to the +`"Models" section in the django documentation +`_. 8. Add swapper configurations ----------------------------- @@ -298,79 +321,95 @@ Once you have created the models, add the following to your ``settings.py``: .. code-block:: python # Setting models for swapper module - CONFIG_DEVICE_MODEL = 'sample_config.Device' - CONFIG_DEVICEGROUP_MODEL = 'sample_config.DeviceGroup' - CONFIG_CONFIG_MODEL = 'sample_config.Config' - CONFIG_TEMPLATETAG_MODEL = 'sample_config.TemplateTag' - CONFIG_TAGGEDTEMPLATE_MODEL = 'sample_config.TaggedTemplate' - CONFIG_TEMPLATE_MODEL = 'sample_config.Template' - CONFIG_VPN_MODEL = 'sample_config.Vpn' - CONFIG_VPNCLIENT_MODEL = 'sample_config.VpnClient' - CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = 'sample_config.OrganizationConfigSettings' - CONFIG_ORGANIZATIONLIMITS_MODEL = 'sample_config.OrganizationLimits' - DJANGO_X509_CA_MODEL = 'sample_pki.Ca' - DJANGO_X509_CERT_MODEL = 'sample_pki.Cert' - GEO_LOCATION_MODEL = 'sample_geo.Location' - GEO_FLOORPLAN_MODEL = 'sample_geo.FloorPlan' - GEO_DEVICELOCATION_MODEL = 'sample_geo.DeviceLocation' - CONNECTION_CREDENTIALS_MODEL = 'sample_connection.Credentials' - CONNECTION_DEVICECONNECTION_MODEL = 'sample_connection.DeviceConnection' - CONNECTION_COMMAND_MODEL = 'sample_connection.Command' - SUBNET_DIVISION_SUBNETDIVISIONRULE_MODEL = 'sample_subnet_division.SubnetDivisionRule' - SUBNET_DIVISION_SUBNETDIVISIONINDEX_MODEL = 'sample_subnet_division.SubnetDivisionIndex' - -Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, -``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. + CONFIG_DEVICE_MODEL = "sample_config.Device" + CONFIG_DEVICEGROUP_MODEL = "sample_config.DeviceGroup" + CONFIG_CONFIG_MODEL = "sample_config.Config" + CONFIG_TEMPLATETAG_MODEL = "sample_config.TemplateTag" + CONFIG_TAGGEDTEMPLATE_MODEL = "sample_config.TaggedTemplate" + CONFIG_TEMPLATE_MODEL = "sample_config.Template" + CONFIG_VPN_MODEL = "sample_config.Vpn" + CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient" + CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings" + CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits" + DJANGO_X509_CA_MODEL = "sample_pki.Ca" + DJANGO_X509_CERT_MODEL = "sample_pki.Cert" + GEO_LOCATION_MODEL = "sample_geo.Location" + GEO_FLOORPLAN_MODEL = "sample_geo.FloorPlan" + GEO_DEVICELOCATION_MODEL = "sample_geo.DeviceLocation" + CONNECTION_CREDENTIALS_MODEL = "sample_connection.Credentials" + CONNECTION_DEVICECONNECTION_MODEL = "sample_connection.DeviceConnection" + CONNECTION_COMMAND_MODEL = "sample_connection.Command" + SUBNET_DIVISION_SUBNETDIVISIONRULE_MODEL = "sample_subnet_division.SubnetDivisionRule" + SUBNET_DIVISION_SUBNETDIVISIONINDEX_MODEL = "sample_subnet_division.SubnetDivisionIndex" + +Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` & +``sample_subnet_division`` with the name you chose in step 1. 9. Create database migrations ----------------------------- -Create database migrations:: +Create database migrations: + +.. code-block:: ./manage.py makemigrations -Now, to use the default ``administrator`` and ``operator`` user groups -like the used in the openwisp_controller module, you'll manually need to make a -migrations file which would look like: +Now, to use the default ``administrator`` and ``operator`` user groups like the used in +the openwisp_controller module, you'll manually need to make a migrations file which +would look like: + +- `sample_config/migrations/0002_default_groups_permissions.py + `_ +- `sample_geo/migrations/0002_default_group_permissions.py + `_ +- `sample_pki/migrations/0002_default_group_permissions.py + `_ +- `sample_connection/migrations/0002_default_group_permissions.py + `_ +- `sample_subnet_division/migrations/0002_default_group_permissions.py + `_ -- `sample_config/migrations/0002_default_groups_permissions.py `_ -- `sample_geo/migrations/0002_default_group_permissions.py `_ -- `sample_pki/migrations/0002_default_group_permissions.py `_ -- `sample_connection/migrations/0002_default_group_permissions.py `_ -- `sample_subnet_division/migrations/0002_default_group_permissions.py `_ +Create database migrations: -Create database migrations:: +.. code-block:: ./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 the admin -------------------- Refer to the ``admin.py`` file of the sample app. -- `sample_config admin.py `_. -- `sample_geo admin.py `_. -- `sample_pki admin.py `_. -- `sample_connection admin.py `_. -- `sample_subnet_division admin.py `_. +- `sample_config admin.py + `_. +- `sample_geo admin.py + `_. +- `sample_pki admin.py + `_. +- `sample_connection admin.py + `_. +- `sample_subnet_division admin.py + `_. -To introduce changes to the admin, you can do it in two main ways which are described below. +To introduce changes to the admin, you can do it in two main ways which are described +below. -**Note**: for more information regarding how the django admin works, or how it can be customized, -please refer to `"The django admin site" section in the django documentation `_. +**Note**: for more information 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: sample_config -^^^^^^^^^^^^^ ++++++++++++++ .. code-block:: python @@ -384,7 +423,7 @@ sample_config # DeviceAdmin.fields += ['example'] <-- monkey patching example sample_connection -^^^^^^^^^^^^^^^^^ ++++++++++++++++++ .. code-block:: python @@ -393,7 +432,7 @@ sample_connection # CredentialsAdmin.fields += ['example'] <-- monkey patching example sample_geo -^^^^^^^^^^ +++++++++++ .. code-block:: python @@ -402,7 +441,7 @@ sample_geo # FloorPlanAdmin.fields += ['example'] <-- monkey patching example sample_pki -^^^^^^^^^^ +++++++++++ .. code-block:: python @@ -411,7 +450,7 @@ sample_pki # CaAdmin.fields += ['example'] <-- monkey patching example sample_subnet_division -^^^^^^^^^^^^^^^^^^^^^^ +++++++++++++++++++++++ .. code-block:: python @@ -420,13 +459,13 @@ sample_subnet_division # SubnetDivisionRuleInlineAdmin.fields += ['example'] <-- monkey patching example 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: sample_config -^^^^^^^^^^^^^ ++++++++++++++ .. code-block:: python @@ -436,105 +475,126 @@ sample_config TemplateAdmin as BaseTemplateAdmin, VpnAdmin as BaseVpnAdmin, DeviceGroupAdmin as BaseDeviceGroupAdmin, + ) from swapper import load_model - Vpn = load_model('openwisp_controller', 'Vpn') - Device = load_model('openwisp_controller', 'Device') - DeviceGroup = load_model('openwisp_controller', 'DeviceGroup') - Template = load_model('openwisp_controller', 'Template') + Vpn = load_model("openwisp_controller", "Vpn") + Device = load_model("openwisp_controller", "Device") + DeviceGroup = load_model("openwisp_controller", "DeviceGroup") + Template = load_model("openwisp_controller", "Template") admin.site.unregister(Vpn) admin.site.unregister(Device) admin.site.unregister(DeviceGroup) admin.site.unregister(Template) + @admin.register(Vpn) class VpnAdmin(BaseVpnAdmin): # add your changes here + pass + @admin.register(Device) class DeviceAdmin(BaseDeviceAdmin): # add your changes here + pass + @admin.register(DeviceGroup) class DeviceGroupAdmin(BaseDeviceGroupAdmin): # add your changes here + pass + @admin.register(Template) class TemplateAdmin(BaseTemplateAdmin): # add your changes here + pass sample_connection -^^^^^^^^^^^^^^^^^ ++++++++++++++++++ .. code-block:: python - from openwisp_controller.connection.admin import CredentialsAdmin as BaseCredentialsAdmin + from openwisp_controller.connection.admin import ( + CredentialsAdmin as BaseCredentialsAdmin, + ) from django.contrib import admin from swapper import load_model - Credentials = load_model('openwisp_controller', 'Credentials') + Credentials = load_model("openwisp_controller", "Credentials") admin.site.unregister(Credentials) + @admin.register(Device) class CredentialsAdmin(BaseCredentialsAdmin): + pass # add your changes here sample_geo -^^^^^^^^^^ +++++++++++ .. code-block:: python from openwisp_controller.geo.admin import ( FloorPlanAdmin as BaseFloorPlanAdmin, - LocationAdmin as BaseLocationAdmin + LocationAdmin as BaseLocationAdmin, ) from django.contrib import admin from swapper import load_model - Location = load_model('openwisp_controller', 'Location') - FloorPlan = load_model('openwisp_controller', 'FloorPlan') + Location = load_model("openwisp_controller", "Location") + FloorPlan = load_model("openwisp_controller", "FloorPlan") admin.site.unregister(FloorPlan) admin.site.unregister(Location) + @admin.register(FloorPlan) class FloorPlanAdmin(BaseFloorPlanAdmin): + pass # add your changes here + @admin.register(Location) class LocationAdmin(BaseLocationAdmin): + pass # add your changes here sample_pki -^^^^^^^^^^ +++++++++++ .. code-block:: python from openwisp_controller.geo.admin import ( CaAdmin as BaseCaAdmin, - CertAdmin as BaseCertAdmin + CertAdmin as BaseCertAdmin, ) from django.contrib import admin from swapper import load_model - Ca = load_model('openwisp_controller', 'Ca') - Cert = load_model('openwisp_controller', 'Cert') + Ca = load_model("openwisp_controller", "Ca") + Cert = load_model("openwisp_controller", "Cert") admin.site.unregister(Ca) admin.site.unregister(Cert) + @admin.register(Ca) class CaAdmin(BaseCaAdmin): + pass # add your changes here + @admin.register(Cert) class CertAdmin(BaseCertAdmin): + pass # add your changes here sample_subnet_division -^^^^^^^^^^^^^^^^^^^^^^ +++++++++++++++++++++++ .. code-block:: python @@ -546,24 +606,30 @@ sample_subnet_division from django.contrib import admin from swapper import load_model - Subnet = load_model('openwisp_ipam', 'Subnet') - IpAddress = load_model('openwisp_ipam', 'IpAddress') - SubnetDivisionRule = load_model('subnet_division', 'SubnetDivisionRule') + Subnet = load_model("openwisp_ipam", "Subnet") + IpAddress = load_model("openwisp_ipam", "IpAddress") + SubnetDivisionRule = load_model("subnet_division", "SubnetDivisionRule") admin.site.unregister(Subnet) admin.site.unregister(IpAddress) admin.site.unregister(SubnetDivisionRule) + @admin.register(Subnet) class SubnetAdmin(BaseSubnetAdmin): + pass # add your changes here + @admin.register(IpAddress) class IpAddressAdmin(BaseIpAddressAdmin): + pass # add your changes here + @admin.register(SubnetDivisionRule) class SubnetDivisionRuleInlineAdmin(BaseSubnetDivisionRuleInlineAdmin): + pass # add your changes here 11. Create root URL configuration @@ -574,6 +640,7 @@ sample_subnet_division from django.contrib import admin from openwisp_controller.config.utils import get_controller_urls from openwisp_controller.geo.utils import get_geo_urls + # from .sample_config import views as config_views # from .sample_geo import views as geo_views @@ -581,51 +648,63 @@ sample_subnet_division # ... other urls in your project ... # Use only when changing controller API views (discussed below) # url(r'^controller/', include((get_controller_urls(config_views), 'controller'), namespace='controller')) - # Use only when changing geo API views (discussed below) # url(r'^geo/', include((get_geo_urls(geo_views), 'geo'), namespace='geo')), - # openwisp-controller urls - url(r'', include(('openwisp_controller.config.urls', 'config'), namespace='config')), - url(r'', include('openwisp_controller.urls')), + url( + r"", include(("openwisp_controller.config.urls", "config"), namespace="config") + ), + url(r"", include("openwisp_controller.urls")), ] -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. 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-controller*. +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-controller*. -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. See the tests in sample_app to find out how to do this. -- `project common tests.py `_ -- `sample_config tests.py `_ -- `sample_geo tests.py `_ -- `sample_geo pytest.py `_ -- `sample_pki tests.py `_ -- `sample_connection tests.py `_ -- `sample_subnet_division tests.py `_ +- `project common tests.py + `_ +- `sample_config tests.py + `_ +- `sample_geo tests.py + `_ +- `sample_geo pytest.py + `_ +- `sample_pki tests.py + `_ +- `sample_connection tests.py + `_ +- `sample_subnet_division tests.py + `_ For running the tests, you need to copy fixtures as well: -- Change `sample_config` to your config app's name in `sample_config fixtures `_ and paste it in the ``sample_config/fixtures/`` directory. +- Change `sample_config` to your config app's name in `sample_config fixtures + `_ + and paste it in the ``sample_config/fixtures/`` directory. + +You can then run tests with: -You can then run tests with:: +.. code-block:: # the --parallel flag is optional ./manage.py test --parallel mycontroller Substitute ``mycontroller`` with the name you chose in step 1. -For more information about automated tests in django, please refer to -`"Testing in Django" `_. +For more information about automated tests in django, please refer to `"Testing in +Django" `_. Other base classes that can be inherited and extended ----------------------------------------------------- @@ -633,39 +712,41 @@ Other base classes that can be inherited and extended The following steps are not required and are intended for more advanced customization. 1. Extending the Controller API Views -##################################### +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Extending the `sample_config/views.py `_ -is required only when you want to make changes in the controller API, -Remember to change ``config_views`` location in ``urls.py`` in point 11 for extending views. +Extending the `sample_config/views.py +`_ +is required only when you want to make changes in the controller API, Remember to change +``config_views`` location in ``urls.py`` in point 11 for extending views. -For more information about django views, please refer to the `views section in the django documentation `_. +For more information about django views, please refer to the `views section in the +django documentation `_. 2. Extending the Geo API Views -############################## +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Extending the `sample_geo/views.py `_ -is required only when you want to make changes in the geo API, -Remember to change ``geo_views`` location in ``urls.py`` in point 11 for extending views. +Extending the `sample_geo/views.py +`_ +is required only when you want to make changes in the geo API, Remember to change +``geo_views`` location in ``urls.py`` in point 11 for extending views. -For more information about django views, please refer to the `views section in the django documentation `_. +For more information about django views, please refer to the `views section in the +django documentation `_. .. _custom_subnet_division_rule_types: Custom Subnet Division Rule Types --------------------------------- -It is possible to create your own -:doc:`subnet division rule types <../user/subnet-division-rules>`. -The rule type determines when subnets and IPs will be provisioned and when they -will be destroyed. +It is possible to create your own :doc:`subnet division rule types +<../user/subnet-division-rules>`. The rule type determines when subnets and IPs will be +provisioned and when they will be destroyed. You can create your custom rule types by extending ``openwisp_controller.subnet_division.rule_types.base.BaseSubnetDivisionRuleType``. -Below is an example to create a subnet division rule type that will provision -subnets and IPs when a new device is created and will delete them upon deletion -for that device. +Below is an example to create a subnet division rule type that will provision subnets +and IPs when a new device is created and will delete them upon deletion for that device. .. code-block:: python @@ -678,7 +759,8 @@ for that device. BaseSubnetDivisionRuleType, ) - Device = load_model('config', 'Device') + Device = load_model("config", "Device") + class CustomRuleType(BaseSubnetDivisionRuleType): # The signal on which provisioning should be triggered @@ -686,14 +768,14 @@ for that device. # The sender of the provision_signal provision_sender = Device # Dispatch UID for connecting provision_signal to provision_receiver - provision_dispatch_uid = 'some_unique_identifier_string' + provision_dispatch_uid = "some_unique_identifier_string" # The signal on which deletion should be triggered destroyer_signal = post_delete # The sender of the destroyer_signal destroyer_sender = Device # Dispatch UID for connecting destroyer_signal to destroyer_receiver - destroyer_dispatch_uid = 'another_unique_identifier_string' + destroyer_dispatch_uid = "another_unique_identifier_string" # Attribute path to organization_id # Example 1: If organization_id is direct attribute of provision_signal @@ -702,38 +784,45 @@ for that device. # Example 2: If organization_id is indirect attribute of provision signal # sender instance, then # organization_id_path = 'some_attribute.another_intermediate.organization_id' - organization_id_path = 'organization_id' + organization_id_path = "organization_id" # Similar to organization_id_path but for the required subnet attribute - subnet_path = 'subnet' + subnet_path = "subnet" # An intermediate method through which you can specify conditions for provisions @classmethod def should_create_subnets_ips(cls, instance, **kwargs): # Using "post_save" provision_signal, the rule should be only # triggered when a new object is created. - return kwargs['created'] + return kwargs["created"] # You can define logic to trigger provisioning for existing objects # using following classmethod. By default, BaseSubnetDivisionRuleType # performs no operation for existing objects. @classmethod def provision_for_existing_objects(cls, rule_obj): - for device in Device.objects.filter( - organization=rule_obj.organization - ): + for device in Device.objects.filter(organization=rule_obj.organization): cls.provision_receiver(device, created=True) After creating a class for your custom rule type, you will need to set -:ref:`OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES ` -setting as follows: +:ref:`OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES +` setting as follows: .. code-block:: python - OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES = ( | - ('openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType', 'VPN'), - ('openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType', 'Device'), - ('mycontroller.sample_subnet_division.rules_types.custom.CustomRuleType', 'Custom Rule'), + OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES = ( + ( + "openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType", + "VPN", + ), + ( + "openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType", + "Device", + ), + ( + "mycontroller.sample_subnet_division.rules_types.custom.CustomRuleType", + "Custom Rule", + ), ) More Utilities to Extend OpenWISP Controller diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 76e8496c4..99915fc96 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -4,13 +4,13 @@ Developer Docs .. include:: ../partials/developer-docs.rst .. toctree:: - :maxdepth: 2 + :maxdepth: 2 - ./installation.rst - ./utils.rst - ./extending.rst + ./installation.rst + ./utils.rst + ./extending.rst Other useful resources: - - :doc:`../user/rest-api` - - :doc:`../user/settings` + - :doc:`../user/rest-api` + - :doc:`../user/settings` diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 81c716a41..e40350e12 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -6,8 +6,8 @@ Developer Installation Instructions Dependencies ------------ -* Python >= 3.8 -* OpenSSL +- Python >= 3.8 +- OpenSSL Installing for development -------------------------- @@ -39,16 +39,15 @@ Launch Redis and PostgreSQL: docker-compose up -d redis postgres -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 your base python packages are up to date -before moving to the next step: +Make sure that your base python packages are up to date before moving to the next step: .. code-block:: shell @@ -62,8 +61,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: @@ -133,10 +133,10 @@ Install and run on Docker .. 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: @@ -158,25 +158,24 @@ You may encounter some issues while installing GeoDjango. Unable to load SpatiaLite library extension? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you are incurring in the following exception:: +If you are incurring in the following exception: - django.core.exceptions.ImproperlyConfigured: Unable to load the SpatiaLite library extension +.. code-block:: -You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` -as explained in + django.core.exceptions.ImproperlyConfigured: Unable to load the SpatiaLite library extension + +You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as explained in `django documentation regarding how to install and configure spatialte `_. Having Issues with other geospatial libraries? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Please refer -`troubleshooting issues related to geospatial libraries +Please refer `troubleshooting issues related to geospatial libraries `_. .. important:: - If you want to add OpenWISP Controller to an - existing Django project, then you can refer to the - `test project in the openwisp-controller repository + If you want to add OpenWISP Controller to an existing Django project, then you can + refer to the `test project in the openwisp-controller repository `_. diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 070e67f7c..e7e8fe71a 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -18,39 +18,37 @@ Signals **Arguments**: - ``instance``: instance of ``Config`` which got its ``config`` modified -- ``previous_status``: indicates the status of the config object before the - signal was emitted -- ``action``: action which emitted the signal, can be any of the list below: - - ``config_changed``: the configuration of the config object was changed - - ``related_template_changed``: the configuration of a related template was changed - - ``m2m_templates_changed``: the assigned templates were changed - (either templates were added, removed or their order was changed) +- ``previous_status``: indicates the status of the config object before the signal was + emitted +- ``action``: action which emitted the signal, can be any of the list below: - + ``config_changed``: the configuration of the config object was changed - + ``related_template_changed``: the configuration of a related template was changed - + ``m2m_templates_changed``: the assigned templates were changed (either templates were + added, removed or their order was changed) This signal is emitted every time the configuration of a device is modified. -It does not matter if ``Config.status`` is already modified, this signal will -be emitted anyway because it signals that the device configuration has changed. +It does not matter if ``Config.status`` is already modified, this signal will be emitted +anyway because it signals that the device configuration has changed. -This signal is used to trigger the update of the configuration on devices, -when the push feature is enabled (requires Device credentials). +This signal is used to trigger the update of the configuration on devices, when the push +feature is enabled (requires Device credentials). -The signal is also emitted when one of the templates used by the device -is modified or if the templates assigned to the device are changed. +The signal is also emitted when one of the templates used by the device is modified or +if the templates assigned to the device are changed. Special cases in which ``config_modified`` is not emitted -######################################################### ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ This signal is not emitted when the device is created for the first time. -It is also not emitted when templates assigned to a config object are -cleared (``post_clear`` m2m signal), this is necessary because -`sortedm2m `_, the package -we use to implement ordered templates, uses the clear action to -reorder templates (m2m relationships are first cleared and then added back), -therefore we ignore ``post_clear`` to avoid emitting signals twice -(one for the clear action and one for the add action). -Please keep this in mind if you plan on using the clear method -of the m2m manager. +It is also not emitted when templates assigned to a config object are cleared +(``post_clear`` m2m signal), this is necessary because `sortedm2m +`_, the package we use to implement +ordered templates, uses the clear action to reorder templates (m2m relationships are +first cleared and then added back), therefore we ignore ``post_clear`` to avoid emitting +signals twice (one for the clear action and one for the add action). Please keep this in +mind if you plan on using the clear method of the m2m manager. ``config_status_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -63,18 +61,16 @@ of the m2m manager. This signal is emitted only when the configuration status of a device has changed. -The signal is emitted also when the m2m template relationships of a config -object are changed, but only on ``post_add`` or ``post_remove`` actions, -``post_clear`` is ignored for the same reason explained -in the previous section. +The signal is emitted also when the m2m template relationships of a config object are +changed, but only on ``post_add`` or ``post_remove`` actions, ``post_clear`` is ignored +for the same reason explained in the previous section. .. _config_backend_changed: ``config_backend_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_controller.config.signals.config_backend_changed`` -**Arguments**: +**Path**: ``openwisp_controller.config.signals.config_backend_changed`` **Arguments**: - ``instance``: instance of ``Config`` which got its ``backend`` changed - ``old_backend``: the old backend of the config object @@ -89,14 +85,14 @@ It is not emitted when the device or config is created. **Arguments**: -- ``instance``: instance of ``Device`` for which its configuration - checksum has been requested +- ``instance``: instance of ``Device`` for which its configuration checksum has been + requested - ``request``: the HTTP request object This signal is emitted when a device requests a checksum via the controller views. -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. ``config_download_requested`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -105,15 +101,15 @@ it is not sent if the response was not successful. **Arguments**: -- ``instance``: instance of ``Device`` for which its configuration has been - requested for download +- ``instance``: instance of ``Device`` for which its configuration has been requested + for download - ``request``: the HTTP request object -This signal is emitted when a device requests to download its configuration -via the controller views. +This signal is emitted when a device requests to download its configuration via the +controller views. -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. ``is_working_changed`` ~~~~~~~~~~~~~~~~~~~~~~ @@ -124,9 +120,10 @@ it is not sent if the response was not successful. - ``instance``: instance of ``DeviceConnection`` - ``is_working``: value of ``DeviceConnection.is_working`` -- ``old_is_working``: previous value of ``DeviceConnection.is_working``, - either ``None`` (for new connections), ``True`` or ``False`` -- ``failure_reason``: error message explaining reason for failure in establishing connection +- ``old_is_working``: previous value of ``DeviceConnection.is_working``, either ``None`` + (for new connections), ``True`` or ``False`` +- ``failure_reason``: error message explaining reason for failure in establishing + connection - ``old_failure_reason``: previous value of ``DeviceConnection.failure_reason`` This signal is emitted every time ``DeviceConnection.is_working`` changes. @@ -156,12 +153,11 @@ It is not triggered when the device is created for the first time. **Arguments**: - ``instance``: instance of ``Device`` which got registered. -- ``is_new``: boolean, will be ``True`` when the device is new, - ``False`` when the device already exists - (eg: a device which gets a factory reset will register again) +- ``is_new``: boolean, will be ``True`` when the device is new, ``False`` when the + device already exists (eg: a device which gets a factory reset will register again) -This signal is emitted when a device registers automatically through the controller -HTTP API. +This signal is emitted when a device registers automatically through the controller HTTP +API. ``device_name_changed`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -216,12 +212,12 @@ It is not emitted when the device is created. **Arguments**: - ``instance``: instance of ``VpnClient``. -- ``provisioned``: dictionary of ``Subnet`` and ``IpAddress`` provisioned, - ``None`` if nothing is provisioned +- ``provisioned``: dictionary of ``Subnet`` and ``IpAddress`` provisioned, ``None`` if + nothing is provisioned -The signal is emitted when subnets and IP addresses have been provisioned -for a ``VpnClient`` for a VPN server with a subnet with -:doc:`subnet division rule <../user/subnet-division-rules>`. +The signal is emitted when subnets and IP addresses have been provisioned for a +``VpnClient`` for a VPN server with a subnet with :doc:`subnet division rule +<../user/subnet-division-rules>`. ``vpn_server_modified`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -245,42 +241,39 @@ The signal is emitted when the VPN server is modified. The signal is emitted when the peers of VPN server gets changed. -It is only emitted for ``Vpn`` object with **WireGuard** or -**VXLAN over WireGuard** backend. +It is only emitted for ``Vpn`` object with **WireGuard** or **VXLAN over WireGuard** +backend. .. _registering_unregistering_commands: Registering / Unregistering Commands ------------------------------------ -OpenWISP Controller allows to register new command options -or unregister existing command options -through two utility functions: +OpenWISP Controller allows to register new command options or unregister existing +command options through two utility functions: - ``openwisp_controller.connection.commands.register_command`` - ``openwisp_controller.connection.commands.unregister_command`` -You can use these functions to register new custom commands -or unregister existing commands from your code. +You can use these functions to register new custom commands or unregister existing +commands from your code. .. note:: - These functions are to be used as an alternative to - the :ref:`OPENWISP_CONTROLLER_USER_COMMANDS` setting - when :doc:`extending openwisp-controller ` - or when developing custom applications based on OpenWISP Controller. + These functions are to be used as an alternative to the + :ref:`OPENWISP_CONTROLLER_USER_COMMANDS` setting when :doc:`extending + openwisp-controller ` or when developing custom applications based on + OpenWISP Controller. ``register_command`` ~~~~~~~~~~~~~~~~~~~~ -+--------------------+------------------------------------------------------------------+ -| Parameter | Description | -+--------------------+------------------------------------------------------------------+ -| ``command_name`` | A ``str`` defining identifier for the command. | -+--------------------+------------------------------------------------------------------+ -| ``command_config`` | A ``dict`` like the one shown in | -| | :ref:`Command Configuration: schema `. | -+--------------------+------------------------------------------------------------------+ +================== ==================================================================== +Parameter Description +``command_name`` A ``str`` defining identifier for the command. +``command_config`` A ``dict`` like the one shown in :ref:`Command Configuration: schema + `. +================== ==================================================================== **Note:** It will raise ``ImproperlyConfigured`` exception if a command is already registered with the same name. @@ -288,39 +281,38 @@ registered with the same name. ``unregister_command`` ~~~~~~~~~~~~~~~~~~~~~~ -+--------------------+-----------------------------------------+ -| Parameter | Description | -+--------------------+-----------------------------------------+ -| ``command_name`` | A ``str`` defining name of the command. | -+--------------------+-----------------------------------------+ +================ ======================================= +Parameter Description +``command_name`` A ``str`` defining name of the command. +================ ======================================= -**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not exists. +**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not +exists. Controller Notifications ------------------------ -The notification types registered and used by OpenWISP Controller are -listed in the following table. +The notification types registered and used by OpenWISP Controller are listed in the +following table. -+-----------------------+----------------------------------------------------------------------+ -| Notification Type | Use | -+-----------------------+----------------------------------------------------------------------+ -| ``config_error`` | Fires when the status of a device configuration changes to ``error``.| -+-----------------------+----------------------------------------------------------------------+ -| ``device_registered`` | Fires when a new device registers itself. | -+-----------------------+----------------------------------------------------------------------+ +===================== ========================================================== +Notification Type Use +``config_error`` Fires when the status of a device configuration changes to + ``error``. +``device_registered`` Fires when a new device registers itself. +===================== ========================================================== Registering notification types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can define your own notification types using -``register_notification_type`` function from OpenWISP Notifications. +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 +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. +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. diff --git a/docs/index.rst b/docs/index.rst index a1667f3c3..7c0823d3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,37 +1,33 @@ Controller ========== -OpenWISP Controller is responsible of of managing -the core resources of the network -and allows automating several aspects like -adoption, provisioning, VPN tunnel configuration, -generation of X509 certificates, -subnet and IP address allocation and more. +OpenWISP Controller is responsible of of managing the core resources of the network and +allows automating several aspects like adoption, provisioning, VPN tunnel configuration, +generation of X509 certificates, subnet and IP address allocation and more. -For a full introduction please refer to -:doc:`user/intro`. +For a full introduction please refer to :doc:`user/intro`. .. toctree:: - :caption: User Docs - :maxdepth: 1 + :caption: User Docs + :maxdepth: 1 - user/intro.rst - user/templates.rst - user/variables.rst - user/device-groups.rst - user/push-operations.rst - user/shell-commands.rst - user/import-export.rst - user/organization-limits.rst - user/wireguard.rst - user/vxlan-wireguard.rst - user/zerotier.rst - user/subnet-division-rules.rst - user/rest-api.rst - user/settings.rst + user/intro.rst + user/templates.rst + user/variables.rst + user/device-groups.rst + user/push-operations.rst + user/shell-commands.rst + user/import-export.rst + user/organization-limits.rst + user/wireguard.rst + user/vxlan-wireguard.rst + user/zerotier.rst + user/subnet-division-rules.rst + user/rest-api.rst + user/settings.rst .. toctree:: - :caption: Developer Docs - :maxdepth: 2 + :caption: Developer Docs + :maxdepth: 2 - Developer Docs Index + Developer Docs Index diff --git a/docs/partials/developer-docs.rst b/docs/partials/developer-docs.rst index 50ddc51ee..4e82c555a 100644 --- a/docs/partials/developer-docs.rst +++ b/docs/partials/developer-docs.rst @@ -1,13 +1,12 @@ .. note:: - This documentation page is aimed at developers who want - to customize, change or extend the code of OpenWISP Controller - 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). + This documentation page is aimed at developers who want to customize, change or + extend the code of OpenWISP Controller 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: + 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 Controller User Docs ` diff --git a/docs/partials/shared-object.rst b/docs/partials/shared-object.rst index fa84c4b16..489ba9ebc 100644 --- a/docs/partials/shared-object.rst +++ b/docs/partials/shared-object.rst @@ -1,8 +1,8 @@ .. note:: - This guide creates the VPN server and VPN client templates - as **Shared systemwide (no organization)** objects. This allows - any device of any organization to use the automation. + This guide creates the VPN server and VPN client templates as **Shared systemwide + (no organization)** objects. This allows any device of any organization to use the + automation. - If needed, you can use any organization as long as the VPN server, - the VPN client template, and devices have the same organization. + If needed, you can use any organization as long as the VPN server, the VPN client + template, and devices have the same organization. diff --git a/docs/user/device-groups.rst b/docs/user/device-groups.rst index c8dee1d0c..c9ca03d18 100644 --- a/docs/user/device-groups.rst +++ b/docs/user/device-groups.rst @@ -1,11 +1,10 @@ Device Groups -------------- +============= -Device groups allow to group similar devices together, -the groups usually share not only a common characteristic but also some -kind of organizational need: they need to have specific configuration -templates, variables and/or associated metadata which differs from the -rest of the network. +Device groups allow to group similar devices together, the groups usually share not only +a common characteristic but also some kind of organizational need: they need to have +specific configuration templates, variables and/or associated metadata which differs +from the rest of the network. Features provided by Device Groups: @@ -14,84 +13,73 @@ Features provided by Device Groups: :local: .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png - :alt: Device Group example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/device-groups.png + :alt: Device Group example .. _device_group_templates: Group Templates -~~~~~~~~~~~~~~~ - -Groups allow to define templates which are automatically assigned to devices -belonging to the group. When using this feature, keep in mind the following -important points: - -- Templates of any configuration backend can be selected, - when a device is assigned to a group, - only the templates which matches the device configuration backend are - applied to the device. -- The system will not force group templates onto devices, this means that - users can remove the applied group templates from a specific device if - needed. -- If a device group is changed, the system will automatically remove the - group templates of the old group and apply the new templates of the new - group (this operation is implemented by leveraging - the :ref:`group_templates_changed` signal). -- If the group templates are changed, the devices which belong to the group - will be automatically updated to reflect the changes - (this operation is executed in a background task). -- In case the configuration backend of a device is changed, - the system will handle this automatically too and update the group - templates accordingly (this operation is implemented by - leveraging the :ref:`config_backend_changed` signal). -- If a device does not have a configuration defined yet, but it is assigned - to a group which has templates defined, the system will automatically - create a configuration for it using the default backend specified - in the :ref:`OPENWISP_CONTROLLER_DEFAULT_BACKEND` setting. - -**Note:** the list of templates shown in the edit group page do not -contain templates flagged -as :ref:`"default" ` -or :ref:`"required" ` -to avoid redundancy because those templates -are automatically assigned by the system -to new devices. - -This feature works also when editing group templates or the group assigned -to a device via the -:ref:`REST API `. +--------------- + +Groups allow to define templates which are automatically assigned to devices belonging +to the group. When using this feature, keep in mind the following important points: + +- Templates of any configuration backend can be selected, when a device is assigned to a + group, only the templates which matches the device configuration backend are applied + to the device. +- The system will not force group templates onto devices, this means that users can + remove the applied group templates from a specific device if needed. +- If a device group is changed, the system will automatically remove the group templates + of the old group and apply the new templates of the new group (this operation is + implemented by leveraging the :ref:`group_templates_changed` signal). +- If the group templates are changed, the devices which belong to the group will be + automatically updated to reflect the changes (this operation is executed in a + background task). +- In case the configuration backend of a device is changed, the system will handle this + automatically too and update the group templates accordingly (this operation is + implemented by leveraging the :ref:`config_backend_changed` signal). +- If a device does not have a configuration defined yet, but it is assigned to a group + which has templates defined, the system will automatically create a configuration for + it using the default backend specified in the + :ref:`OPENWISP_CONTROLLER_DEFAULT_BACKEND` setting. + +**Note:** the list of templates shown in the edit group page do not contain templates +flagged as :ref:`"default" ` or :ref:`"required" +` to avoid redundancy because those templates are automatically +assigned by the system to new devices. + +This feature works also when editing group templates or the group assigned to a device +via the :ref:`REST API `. .. _device_group_variables: Group Configuration Variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- -Groups allow to define configuration variables which are automatically -added to the device's context in the **System Defined Variables**. -Check the :doc:`./variables` section -to learn more about precedence of different configuration variables. +Groups allow to define configuration variables which are automatically added to the +device's context in the **System Defined Variables**. Check the :doc:`./variables` +section to learn more about precedence of different configuration variables. -This feature also works when editing group templates or the group assigned -to a device via the :ref:`REST API `. +This feature also works when editing group templates or the group assigned to a device +via the :ref:`REST API `. Group Metadata -~~~~~~~~~~~~~~ +-------------- -Groups allow to store additional information regarding a group in the -structured metadata field (which can be accessed via the REST API). +Groups allow to store additional information regarding a group in the structured +metadata field (which can be accessed via the REST API). -The metadata field allows custom structure and validation to -standardize information across all groups using -the :ref:`OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA` setting. +The metadata field allows custom structure and validation to standardize information +across all groups using the :ref:`OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA` setting. Variables vs Metadata -~~~~~~~~~~~~~~~~~~~~~ +--------------------- *Group configuration variables* and *Group metadata* serves different purposes. -The group configuration variables should be used when the device configuration is required -to be changed for particular group of devices. +The group configuration variables should be used when the device configuration is +required to be changed for particular group of devices. -Group metadata should be used to store additional data for the device -group, this data can be fetched and/or tweaked via the REST API if needed. -Group metadata is not designed to be used for configuration purposes. +Group metadata should be used to store additional data for the device group, this data +can be fetched and/or tweaked via the REST API if needed. Group metadata is not designed +to be used for configuration purposes. diff --git a/docs/user/import-export.rst b/docs/user/import-export.rst index 28daeb8e6..f986a379c 100644 --- a/docs/user/import-export.rst +++ b/docs/user/import-export.rst @@ -2,26 +2,28 @@ Import/Export Device Data ========================= .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png - :alt: Import / Export + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png + :alt: Import / Export -The device list page offers two buttons to export and import device data in -different formats. +The device list page offers two buttons to export and import device data in different +formats. Importing --------- -For importing devices into the system, only the required fields are needed, -for example, the following CSV file will import a device named -``TestImport`` with mac address ``00:11:22:09:44:55`` in the organization with -UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``:: +For importing devices into the system, only the required fields are needed, for example, +the following CSV file will import a device named ``TestImport`` with mac address +``00:11:22:09:44:55`` in the organization with UUID +``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``: + +.. code-block:: organization,name,mac_address 3cb5e18c-0312-48ab-8dbd-038b8415bd6f,TestImport,00:11:22:09:44:55 .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png - :alt: Import / Export + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/import-page.png + :alt: Import / Export Exporting --------- @@ -29,5 +31,5 @@ Exporting The export feature respects any filters selected in the device list. .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png - :alt: Export + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/export-page.png + :alt: Export diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 947c32422..5ede6f1a7 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -1,8 +1,7 @@ Controller: Structure & Features ================================ -OpenWISP Controller is a Python package -which ships five Django apps. +OpenWISP Controller is a Python package which ships five Django apps. .. contents:: :depth: 1 @@ -11,71 +10,65 @@ which ships five Django apps. Config App ---------- -The config app is the core of the controller module and implements all -the following features: - -* **Configuration management** for embedded devices supporting: - - `OpenWRT `_ - - `OpenWISP Firmware `_ - - additional firmware can be added by - :ref:`specifying custom configuration - backends ` -* **Configuration editor** based on - `JSON-Schema editor `_ -* **Advanced edit mode**: edit - `NetJSON `_ - *DeviceConfiguration* objects for maximum flexibility -* :doc:`templates`: - reduce repetition to the minimum, configure default and required templates -* :doc:`variables`: - reference variables in the configuration and templates -* :doc:`device-groups`: define different set of default configuration - and metadata in device groups -* :ref:`Template Tags `: define different - sets of default templates (eg: mesh, WDS, 4G) -* **HTTP resources**: allow devices to automatically check for and - download configuration updates -* **VPN management**: automatically provision VPN tunnel configurations, - including cryptographic keys and IP addresses, - eg: :doc:`OpenVPN `, :doc:`WireGuard ` -* :doc:`import-export` +The config app is the core of the controller module and implements all the following +features: + +- **Configuration management** for embedded devices supporting: + - `OpenWRT `_ + - `OpenWISP Firmware `_ + - additional firmware can be added by :ref:`specifying custom configuration + backends ` +- **Configuration editor** based on `JSON-Schema editor + `_ +- **Advanced edit mode**: edit `NetJSON `_ *DeviceConfiguration* + objects for maximum flexibility +- :doc:`templates`: reduce repetition to the minimum, configure default and required + templates +- :doc:`variables`: reference variables in the configuration and templates +- :doc:`device-groups`: define different set of default configuration and metadata in + device groups +- :ref:`Template Tags `: define different sets of default templates (eg: + mesh, WDS, 4G) +- **HTTP resources**: allow devices to automatically check for and download + configuration updates +- **VPN management**: automatically provision VPN tunnel configurations, including + cryptographic keys and IP addresses, eg: :doc:`OpenVPN `, :doc:`WireGuard + ` +- :doc:`import-export` It exposes various :doc:`REST API endpoints `. PKI App ------- -The PKI app is based on `django-x509 -`_, allowing you to create, import, -and view x509 CAs and certificates directly from the administration -dashboard. +The PKI app is based on `django-x509 `_, +allowing you to create, import, and view x509 CAs and certificates directly from the +administration dashboard. It exposes various :doc:`REST API endpoints `. Connection App -------------- -This app enables OpenWISP Controller to use different protocols to reach -network devices. Currently, the default connection protocols are -SSH and SNMP, but the protocol mechanism is extensible, -allowing for implementation of additional protocols if needed. +This app enables OpenWISP Controller to use different protocols to reach network +devices. Currently, the default connection protocols are SSH and SNMP, but the protocol +mechanism is extensible, allowing for implementation of additional protocols if needed. It exposes various :doc:`REST API endpoints `. SSH ~~~ -The SSH connector allows the controller to initialize connections -to the devices in order to perform -:doc:`push operations `, e.g.: +The SSH connector allows the controller to initialize connections to the devices in +order to perform :doc:`push operations `, e.g.: - Sending configuration updates. - :doc:`Executing shell commands `. -- Perform firmware upgrades via the additional - :doc:`firmware upgrade module `. +- Perform firmware upgrades via the additional :doc:`firmware upgrade module + `. -The default connection protocol implemented is SSH, but other protocol -mechanism is extensible and custom protocols can be implemented as well. +The default connection protocol implemented is SSH, but other protocol mechanism is +extensible and custom protocols can be implemented as well. Access via SSH key is recommended, the SSH key algorithms supported are: @@ -85,20 +78,18 @@ Access via SSH key is recommended, the SSH key algorithms supported are: SNMP ~~~~ -The SNMP connector is useful to collect monitoring information -and it's used in -:doc:`OpenWISP Monitoring ` -for performing checks to collect monitoring information. -`Read more `_ -on how to use it. +The SNMP connector is useful to collect monitoring information and it's used in +:doc:`OpenWISP Monitoring ` for performing +checks to collect monitoring information. `Read more +`_ on +how to use it. Geo App ------- -The geographic app is based on -`django-loci `_ -and allows to define the geographic coordinates of the devices, -as well as their indoor coordinates on floorplan images. +The geographic app is based on `django-loci `_ +and allows to define the geographic coordinates of the devices, as well as their indoor +coordinates on floorplan images. It exposes various :doc:`REST API endpoints `. @@ -107,16 +98,15 @@ Subnet Division App .. note:: - This app is optional, if you don't need it you - can avoid adding it to ``settings.INSTALLED_APPS``. + This app is optional, if you don't need it you can avoid adding it to + ``settings.INSTALLED_APPS``. -This app allows to automatically provision subnets and IP -addresses which will be available as -:ref:`system defined configuration variables ` +This app allows to automatically provision subnets and IP addresses which will be +available as :ref:`system defined configuration variables ` that can be used in :doc:`templates`. -The purpose of this app is to allow users to automatically -provision and configure specific subnets and IP addresses -to the devices without the need of manual intervention. +The purpose of this app is to allow users to automatically provision and configure +specific subnets and IP addresses to the devices without the need of manual +intervention. Refer to :doc:`subnet-division-rules` for more information. diff --git a/docs/user/organization-limits.rst b/docs/user/organization-limits.rst index 74d556b02..2b3a9d98b 100644 --- a/docs/user/organization-limits.rst +++ b/docs/user/organization-limits.rst @@ -1,5 +1,5 @@ Organization Limits -------------------- +=================== You can restrict the number of devices managed by each organization. @@ -13,5 +13,5 @@ To set these limits: Refer to the screenshot below for guidance: .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png - :alt: Organization limits + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/organization-limits.png + :alt: Organization limits diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index ba695de49..f9d9eee50 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -1,100 +1,98 @@ Configuring Push Operations ---------------------------- +=========================== .. important:: - If you have installed OpenWISP with the - `ansbile-openwisp2 role `_ - you can skip the following steps, which are handled automatically - by the ansible role during the first installation. + If you have installed OpenWISP with the `ansbile-openwisp2 role + `_ you can skip the following steps, + which are handled automatically by the ansible role during the first installation. -The Ansible role automatically creates a default template -to update ``authorized_keys`` on networking devices using the -default access credentials. +The Ansible role automatically creates a default template to update ``authorized_keys`` +on networking devices using the default access credentials. Follow the procedure described below to enable secure SSH access from OpenWISP to your -devices, this is required to enable push operations -(whenever the configuration is changed, OpenWISP will trigger -the update in the background) and/or -:doc:`firmware upgrades (via the additional -module openwisp-firmware-upgrader) <../../../openwisp-firmware-upgrader/docs/index>`. +devices, this is required to enable push operations (whenever the configuration is +changed, OpenWISP will trigger the update in the background) and/or :doc:`firmware +upgrades (via the additional module openwisp-firmware-upgrader) +<../../../openwisp-firmware-upgrader/docs/index>`. 1. Generate SSH key -~~~~~~~~~~~~~~~~~~~ +------------------- -First of all, we need to generate the SSH key which will be -used by OpenWISP to access the devices, to do so, you can use the following command: +First of all, we need to generate the SSH key which will be used by OpenWISP to access +the devices, to do so, you can use the following command: .. code-block:: shell echo './sshkey' | ssh-keygen -t ed25519 -C "openwisp" -This will create two files in the current directory, one called ``sshkey`` -(the private key) and one called -``sshkey.pub`` (the public key). +This will create two files in the current directory, one called ``sshkey`` (the private +key) and one called ``sshkey.pub`` (the public key). Store the content of these files in a secure location. -**Note:** Support for **ED25519** was added in OpenWrt 21.02 (requires Dropbear > 2020.79). -If you are managing devices with OpenWrt < 21, then you will need to use RSA keys: +**Note:** Support for **ED25519** was added in OpenWrt 21.02 (requires Dropbear > +2020.79). If you are managing devices with OpenWrt < 21, then you will need to use RSA +keys: .. code-block:: shell echo './sshkey' | ssh-keygen -t rsa -b 4096 -C "openwisp" 2. Save SSH private key in OpenWISP (access credentials) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png - :alt: add SSH private key as access credential in OpenWISP + :alt: add SSH private key as access credential in OpenWISP -From the first page of OpenWISP click on "Access credentials", then click -on the **"ADD ACCESS CREDENTIALS"** button in the upper right corner -(alternatively, go to the following URL: ``/admin/connection/credentials/add/``). +From the first page of OpenWISP click on "Access credentials", then click on the **"ADD +ACCESS CREDENTIALS"** button in the upper right corner (alternatively, go to the +following URL: ``/admin/connection/credentials/add/``). -Select SSH as ``type``, enable the **Auto add** checkbox, then at the field -"Credentials type" select "SSH (private key)", now type "root" in the ``username`` field, -while in the ``key`` field you have to paste the contents of the private key just created. +Select SSH as ``type``, enable the **Auto add** checkbox, then at the field "Credentials +type" select "SSH (private key)", now type "root" in the ``username`` field, while in +the ``key`` field you have to paste the contents of the private key just created. Now hit save. -The credentials just created will be automatically enabled for all the devices in the system -(both existing devices and devices which will be added in the future). +The credentials just created will be automatically enabled for all the devices in the +system (both existing devices and devices which will be added in the future). 3. Add the public key to your devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png - :alt: Add authorized SSH public keys template to OpenWISP (OpenWRT) + :alt: Add authorized SSH public keys template to OpenWISP (OpenWRT) -Now we need to instruct your devices to allow OpenWISP accessing via SSH, -in order to do this we need to add the contents of the public key file created in step 1 +Now we need to instruct your devices to allow OpenWISP accessing via SSH, in order to do +this we need to add the contents of the public key file created in step 1 (``sshkey.pub``) in the file ``/etc/dropbear/authorized_keys`` on the devices, the -recommended way to do this is to create a configuration template in OpenWISP: -from the first page of OpenWISP, click on "Templates", then and click on the -**"ADD TEMPLATE"** button in the upper right corner (alternatively, go to the following URL: +recommended way to do this is to create a configuration template in OpenWISP: from the +first page of OpenWISP, click on "Templates", then and click on the **"ADD TEMPLATE"** +button in the upper right corner (alternatively, go to the following URL: ``/admin/config/template/add/``). -Check **enabled by default**, then scroll down the configuration section, -click on "Configuration Menu", scroll down, click on "Files" then close the menu -by clicking again on "Configuration Menu". Now type ``/etc/dropbear/authorized_keys`` -in the ``path`` field of the file, then paste the contents of ``sshkey.pub`` in ``contents``. +Check **enabled by default**, then scroll down the configuration section, click on +"Configuration Menu", scroll down, click on "Files" then close the menu by clicking +again on "Configuration Menu". Now type ``/etc/dropbear/authorized_keys`` in the +``path`` field of the file, then paste the contents of ``sshkey.pub`` in ``contents``. Now hit save. **There's a catch**: you will need to assign the template to any existing device. 4. Test it -~~~~~~~~~~ +---------- Once you have performed the 3 steps above, you can test it as follows: -1. Ensure there's at least one device turned on and connected to OpenWISP, ensure - this device has the "SSH Authorized Keys" assigned to it. -2. Ensure the celery worker of OpenWISP Controller is running (eg: ``ps aux | grep celery``) -3. SSH into the device and wait (maximum 2 minutes) until ``/etc/dropbear/authorized_keys`` - appears as specified in the template. +1. Ensure there's at least one device turned on and connected to OpenWISP, ensure this + device has the "SSH Authorized Keys" assigned to it. +2. Ensure the celery worker of OpenWISP Controller is running (eg: ``ps aux | grep + celery``) +3. SSH into the device and wait (maximum 2 minutes) until + ``/etc/dropbear/authorized_keys`` appears as specified in the template. 4. While connected via SSH to the device run the following command in the console: ``logread -f``, now try changing the device name in OpenWISP -5. Shortly after you change the name in OpenWISP, you should see some output in the - SSH console indicating another SSH access and the configuration update being performed. +5. Shortly after you change the name in OpenWISP, you should see some output in the SSH + console indicating another SSH access and the configuration update being performed. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 2622ecf54..bb1aa6815 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -8,7 +8,8 @@ Live documentation .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.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/``. .. _controller_browsable_web_interface: @@ -17,27 +18,26 @@ Browsable web interface .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.png -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. +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. Authentication -------------- See :ref:`authenticating_rest_api`. -When browsing the API via the :ref:`controller_live_documentation` -or the :ref:`controller_browsable_web_interface`, you can also use -the session authentication by logging in the django admin. +When browsing the API via the :ref:`controller_live_documentation` or the +:ref:`controller_browsable_web_interface`, you can also use the session authentication +by logging in the django admin. Pagination ---------- -All *list* endpoints support the ``page_size`` parameter that allows paginating -the results in conjunction with the ``page`` parameter. +All *list* endpoints support the ``page_size`` parameter that allows paginating the +results in conjunction with the ``page`` parameter. .. code-block:: text @@ -49,14 +49,10 @@ the results in conjunction with the ``page`` parameter. List of endpoints ----------------- -Since the detailed explanation is contained in the -:ref:`controller_live_documentation` -and in the -:ref:`controller_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:`controller_live_documentation` +and in the :ref:`controller_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. List devices ~~~~~~~~~~~~ @@ -67,65 +63,64 @@ List devices **Available filters** -You can filter a list of devices based on their configuration -status using the ``status`` (e.g modified, applied, or error). +You can filter a list of devices based on their configuration status using the +``status`` (e.g modified, applied, or error). .. code-block:: text - GET /api/v1/controller/device/?config__status={status} + GET /api/v1/controller/device/?config__status={status} -You can filter a list of devices based on their configuration backend -using the ``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). +You can filter a list of devices based on their configuration backend using the +``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). .. code-block:: text - GET /api/v1/controller/device/?config__backend={backend} + GET /api/v1/controller/device/?config__backend={backend} -You can filter a list of devices based on their -organization using the ``organization_id`` or ``organization_slug``. +You can filter a list of devices based on their organization using the +``organization_id`` or ``organization_slug``. .. code-block:: text - GET /api/v1/controller/device/?organization={organization_id} + GET /api/v1/controller/device/?organization={organization_id} .. code-block:: text - GET /api/v1/controller/device/?organization_slug={organization_slug} + GET /api/v1/controller/device/?organization_slug={organization_slug} -You can filter a list of devices based on their -configuration templates using the ``template_id``. +You can filter a list of devices based on their configuration templates using the +``template_id``. .. code-block:: text - GET /api/v1/controller/device/?config__templates={template_id} + GET /api/v1/controller/device/?config__templates={template_id} -You can filter a list of devices based on -their device group using the ``group_id``. +You can filter a list of devices based on their device group using the ``group_id``. .. code-block:: text - GET /api/v1/controller/device/?group={group_id} + GET /api/v1/controller/device/?group={group_id} -You can filter a list of devices that have a device -location object using the ``with_geo`` (eg. true or false). +You can filter a list of devices that have a device location object using the +``with_geo`` (eg. true or false). .. code-block:: text - GET /api/v1/controller/device/?with_geo={with_geo} + GET /api/v1/controller/device/?with_geo={with_geo} -You can filter a list of devices based on -their creation time using the ``creation_time``. +You can filter a list of devices based on their creation time using the +``creation_time``. .. code-block:: text - # Created exact - GET /api/v1/controller/device/?created={creation_time} + # Created exact + GET /api/v1/controller/device/?created={creation_time} - # Created greater than or equal to - GET /api/v1/controller/device/?created__gte={creation_time} + # Created greater than or equal to + GET /api/v1/controller/device/?created__gte={creation_time} - # Created is less than - GET /api/v1/controller/device/?created__lt={creation_time} + # Created is less than + GET /api/v1/controller/device/?created__lt={creation_time} Create device ~~~~~~~~~~~~~ @@ -148,7 +143,8 @@ Download device configuration GET /api/v1/controller/device/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific device. +The above endpoint triggers the download of a ``tar.gz`` file containing the generated +configuration for that specific device. Change details of device ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -165,13 +161,15 @@ Patch details of device PATCH /api/v1/controller/device/{id}/ **Note**: To assign, unassign, and change the order of the assigned templates add, -remove, and change the order of the ``{id}`` of the templates under the ``config`` field in the JSON response respectively. -Moreover, you can also select and unselect templates in the HTML Form of the Browsable API. +remove, and change the order of the ``{id}`` of the templates under the ``config`` field +in the JSON response respectively. Moreover, you can also select and unselect templates +in the HTML Form of the Browsable API. The required template(s) from the organization(s) of the device will added automatically to the ``config`` and cannot be removed. -**Example usage**: For assigning template(s) add the/their {id} to the config of a device, +**Example usage**: For assigning template(s) add the/their {id} to the config of a +device, .. code-block:: shell @@ -185,7 +183,8 @@ to the ``config`` and cannot be removed. } }' -**Example usage**: For removing assigned templates, simply remove the/their {id} from the config of a device, +**Example usage**: For removing assigned templates, simply remove the/their {id} from +the config of a device, .. code-block:: shell @@ -199,7 +198,8 @@ to the ``config`` and cannot be removed. } }' -**Example usage**: For reordering the templates simply change their order from the config of a device, +**Example usage**: For reordering the templates simply change their order from the +config of a device, .. code-block:: shell @@ -339,24 +339,23 @@ List device groups **Available filters** -You can filter a list of device groups based on their -organization using the ``organization_id`` or ``organization_slug``. +You can filter a list of device groups based on their organization using the +``organization_id`` or ``organization_slug``. .. code-block:: text - GET /api/v1/controller/group/?organization={organization_id} + GET /api/v1/controller/group/?organization={organization_id} .. code-block:: text - GET /api/v1/controller/group/?organization_slug={organization_slug} + GET /api/v1/controller/group/?organization_slug={organization_slug} -You can filter a list of device groups that have a -device object using the ``empty`` (eg. true or false). +You can filter a list of device groups that have a device object using the ``empty`` +(eg. true or false). .. code-block:: text - GET /api/v1/controller/group/?empty={empty} - + GET /api/v1/controller/group/?empty={empty} Create device group ~~~~~~~~~~~~~~~~~~~ @@ -381,8 +380,7 @@ Change device group detail PUT /api/v1/controller/group/{id}/ -This endpoint allows to change the -:ref:`device_group_templates` too. +This endpoint allows to change the :ref:`device_group_templates` too. Get device group from certificate common name ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -391,12 +389,12 @@ Get device group from certificate common name GET /api/v1/controller/cert/{common_name}/group/ -This endpoint can be used to retrieve group information and metadata by the -common name of a certificate used in a VPN client tunnel, this endpoint is -used in layer 2 tunneling solutions for firewall/captive portals. +This endpoint can be used to retrieve group information and metadata by the common name +of a certificate used in a VPN client tunnel, this endpoint is used in layer 2 tunneling +solutions for firewall/captive portals. -It is also possible to filter device group by providing organization slug -of certificate's organization as show in the example below: +It is also possible to filter device group by providing organization slug of +certificate's organization as show in the example below: .. code-block:: text @@ -407,7 +405,6 @@ Get device location .. code-block:: text - GET /api/v1/controller/device/{id}/location/ .. _create_device_location: @@ -419,9 +416,8 @@ Create device location PUT /api/v1/controller/device/{id}/location/ -You can create ``DeviceLocation`` object by using primary -keys of existing ``Location`` and ``FloorPlan`` objects as shown in -the example below. +You can create ``DeviceLocation`` object by using primary keys of existing ``Location`` +and ``FloorPlan`` objects as shown in the example below. .. code-block:: json @@ -431,10 +427,9 @@ the example below. "indoor": "-36,264" } -**Note:** The ``indoor`` field represents the coordinates of the -point placed on the image from the top left corner. E.g. if you -placed the pointer on the top left corner of the floorplan image, -its indoor coordinates will be ``0,0``. +**Note:** The ``indoor`` field represents the coordinates of the point placed on the +image from the top left corner. E.g. if you placed the pointer on the top left corner of +the floorplan image, its indoor coordinates will be ``0,0``. .. code-block:: text @@ -448,11 +443,10 @@ its indoor coordinates will be ``0,0``. "indoor": "-36,264" }' -You can also create related ``Location`` and ``FloorPlan`` objects for the -device directly from this endpoint. +You can also create related ``Location`` and ``FloorPlan`` objects for the device +directly from this endpoint. -The following example demonstrates creating related location -object in a single request. +The following example demonstrates creating related location object in a single request. .. code-block:: json @@ -486,8 +480,8 @@ object in a single request. } }' -**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** -format, like following: +**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** format, +like following: .. code-block:: json @@ -500,10 +494,10 @@ format, like following: } } -Similarly, you can create ``Floorplan`` object with the same request. -But, note that a ``FloorPlan`` can be added to ``DeviceLocation`` only -if the related ``Location`` object defines an indoor location. The example -below demonstrates creating both ``Location`` and ``FloorPlan`` objects. +Similarly, you can create ``Floorplan`` object with the same request. But, note that a +``FloorPlan`` can be added to ``DeviceLocation`` only if the related ``Location`` object +defines an indoor location. The example below demonstrates creating both ``Location`` +and ``FloorPlan`` objects. .. code-block:: text @@ -533,11 +527,11 @@ below demonstrates creating both ``Location`` and ``FloorPlan`` objects. -F floorplan.floor=1 \ -F 'floorplan.image=@floorplan.png' -**Note:** The request in above example uses ``multipart content-type`` -for uploading floorplan image. +**Note:** The request in above example uses ``multipart content-type`` for uploading +floorplan image. -You can also use an existing ``Location`` object and create a new -floorplan for that location using this endpoint. +You can also use an existing ``Location`` object and create a new floorplan for that +location using this endpoint. .. code-block:: text @@ -566,11 +560,9 @@ Change details of device location PUT /api/v1/controller/device/{id}/location/ -**Note:** This endpoint can be used to update related ``Location`` -and ``Floorplan`` objects. Refer to the -:ref:`examples in the "Create device location" -section ` -for information on payload format. +**Note:** This endpoint can be used to update related ``Location`` and ``Floorplan`` +objects. Refer to the :ref:`examples in the "Create device location" section +` for information on payload format. Delete device location ~~~~~~~~~~~~~~~~~~~~~~ @@ -588,9 +580,8 @@ Get device coordinates **Note:** This endpoint is intended to be used by devices. -This endpoint skips multi-tenancy and permission checks if the -device ``key`` is passed as ``query_param`` because the system -assumes that the device is updating it's position. +This endpoint skips multi-tenancy and permission checks if the device ``key`` is passed +as ``query_param`` because the system assumes that the device is updating it's position. .. code-block:: text @@ -606,9 +597,8 @@ Update device coordinates **Note:** This endpoint is intended to be used by devices. -This endpoint skips multi-tenancy and permission checks if the -device ``key`` is passed as ``query_param`` because the system -assumes that the device is updating it's position. +This endpoint skips multi-tenancy and permission checks if the device ``key`` is passed +as ``query_param`` because the system assumes that the device is updating it's position. .. code-block:: json @@ -642,8 +632,8 @@ List locations **Available filters** -You can filter using ``organization_id`` or ``organization_slug`` -to get list locations that belongs to an organization. +You can filter using ``organization_id`` or ``organization_slug`` to get list locations +that belongs to an organization. .. code-block:: text @@ -660,11 +650,11 @@ Create location POST /api/v1/controller/location/ -If you are creating an ``indoor`` location, you can use this endpoint -to create floorplan for the location. +If you are creating an ``indoor`` location, you can use this endpoint to create +floorplan for the location. -The following example demonstrates creating floorplan along with location -in a single request. +The following example demonstrates creating floorplan along with location in a single +request. .. code-block:: text @@ -696,8 +686,8 @@ in a single request. -F 'floorplan.image=@floorplan.png' \ -F organization=1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05 -**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** -format, like following: +**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** format, +like following: .. code-block:: text @@ -726,14 +716,11 @@ Change location details PUT /api/v1/controller/location/{pk}/ -**Note**: Only the first floorplan data present can be -edited or changed. Setting the ``type`` of location to -outdoor will remove all the floorplans associated with it. +**Note**: Only the first floorplan data present can be edited or changed. Setting the +``type`` of location to outdoor will remove all the floorplans associated with it. -Refer to the -:ref:`examples in the "Create device location" -section ` -for information on payload format. +Refer to the :ref:`examples in the "Create device location" section +` for information on payload format. Delete location ~~~~~~~~~~~~~~~ @@ -760,8 +747,8 @@ List locations with devices deployed (in GeoJSON format) **Available filters** -You can filter using ``organization_id`` or ``organization_slug`` -to get list location of devices from that organization. +You can filter using ``organization_id`` or ``organization_slug`` to get list location +of devices from that organization. .. code-block:: text @@ -780,8 +767,8 @@ List floorplans **Available filters** -You can filter using ``organization_id`` or ``organization_slug`` -to get list floorplans that belongs to an organization. +You can filter using ``organization_id`` or ``organization_slug`` to get list floorplans +that belongs to an organization. .. code-block:: text @@ -828,61 +815,61 @@ List templates **Available filters** -You can filter a list of templates based on their organization -using the ``organization_id`` or ``organization_slug``. +You can filter a list of templates based on their organization using the +``organization_id`` or ``organization_slug``. .. code-block:: text - GET /api/v1/controller/template/?organization={organization_id} + GET /api/v1/controller/template/?organization={organization_id} .. code-block:: text GET /api/v1/controller/template/?organization_slug={organization_slug} -You can filter a list of templates based on their backend using -the ``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). +You can filter a list of templates based on their backend using the ``backend`` (e.g +netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). .. code-block:: text - GET /api/v1/controller/template/?backend={backend} + GET /api/v1/controller/template/?backend={backend} -You can filter a list of templates based on their -type using the ``type`` (eg. vpn or generic). +You can filter a list of templates based on their type using the ``type`` (eg. vpn or +generic). .. code-block:: text - GET /api/v1/controller/template/?type={type} + GET /api/v1/controller/template/?type={type} -You can filter a list of templates that are enabled -by default or not using the ``default`` (eg. true or false). +You can filter a list of templates that are enabled by default or not using the +``default`` (eg. true or false). .. code-block:: text - GET /api/v1/controller/template/?default={default} + GET /api/v1/controller/template/?default={default} -You can filter a list of templates that are required -or not using the ``required`` (eg. true or false). +You can filter a list of templates that are required or not using the ``required`` (eg. +true or false). .. code-block:: text - GET /api/v1/controller/template/?required={required} + GET /api/v1/controller/template/?required={required} -You can filter a list of templates based on -their creation time using the ``creation_time``. +You can filter a list of templates based on their creation time using the +``creation_time``. .. code-block:: text - # Created exact + # Created exact - GET /api/v1/controller/template/?created={creation_time} + GET /api/v1/controller/template/?created={creation_time} - # Created greater than or equal to + # Created greater than or equal to - GET /api/v1/controller/template/?created__gte={creation_time} + GET /api/v1/controller/template/?created__gte={creation_time} - # Created is less than + # Created is less than - GET /api/v1/controller/template/?created__lt={creation_time} + GET /api/v1/controller/template/?created__lt={creation_time} Create template ~~~~~~~~~~~~~~~ @@ -905,8 +892,8 @@ Download template configuration GET /api/v1/controller/template/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file -containing the generated configuration for that specific template. +The above endpoint triggers the download of a ``tar.gz`` file containing the generated +configuration for that specific template. Change details of template ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -938,27 +925,25 @@ List VPNs **Available filters** -You can filter a list of vpns based -on their backend using the ``backend`` -(e.g openwisp_controller.vpn_backends.OpenVpn -or openwisp_controller.vpn_backends.Wireguard). +You can filter a list of vpns based on their backend using the ``backend`` (e.g +openwisp_controller.vpn_backends.OpenVpn or openwisp_controller.vpn_backends.Wireguard). .. code-block:: text - GET /api/v1/controller/vpn/?backend={backend} + GET /api/v1/controller/vpn/?backend={backend} You can filter a list of vpns based on their subnet using the ``subnet_id``. .. code-block:: text - GET /api/v1/controller/vpn/?subnet={subnet_id} + GET /api/v1/controller/vpn/?subnet={subnet_id} -You can filter a list of vpns based on their organization -using the ``organization_id`` or ``organization_slug``. +You can filter a list of vpns based on their organization using the ``organization_id`` +or ``organization_slug``. .. code-block:: text - GET /api/v1/controller/vpn/?organization={organization_id} + GET /api/v1/controller/vpn/?organization={organization_id} .. code-block:: text @@ -985,8 +970,8 @@ Download VPN configuration GET /api/v1/controller/vpn/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file -containing the generated configuration for that specific VPN. +The above endpoint triggers the download of a ``tar.gz`` file containing the generated +configuration for that specific VPN. Change details of VPN ~~~~~~~~~~~~~~~~~~~~~ @@ -1030,9 +1015,8 @@ Import existing CA POST /api/v1/controller/ca/ -**Note**: To import an existing CA, only ``name``, ``certificate`` -and ``private_key`` fields have to be filled in the ``HTML`` form or -included in the ``JSON`` format. +**Note**: To import an existing CA, only ``name``, ``certificate`` and ``private_key`` +fields have to be filled in the ``HTML`` form or included in the ``JSON`` format. Get CA Detail ~~~~~~~~~~~~~ @@ -1062,8 +1046,8 @@ Download CA(crl) GET /api/v1/controller/ca/{id}/crl/ -The above endpoint triggers the download of ``{id}.crl`` file containing -up to date CRL of that specific CA. +The above endpoint triggers the download of ``{id}.crl`` file containing up to date CRL +of that specific CA. Delete CA ~~~~~~~~~ @@ -1100,9 +1084,9 @@ Import existing Cert POST /api/v1/controller/cert/ -**Note**: To import an existing Cert, only ``name``, ``ca``, -``certificate`` and ``private_key`` fields have to be filled -in the ``HTML`` form or included in the ``JSON`` format. +**Note**: To import an existing Cert, only ``name``, ``ca``, ``certificate`` and +``private_key`` fields have to be filled in the ``HTML`` form or included in the +``JSON`` format. Get Cert Detail ~~~~~~~~~~~~~~~ diff --git a/docs/user/settings.rst b/docs/user/settings.rst index a651d141e..58f88b292 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -1,723 +1,690 @@ Settings --------- +======== .. include:: /partials/settings-note.rst ``OPENWISP_SSH_AUTH_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``2`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ +============ =========== +**type**: ``int`` +**default**: ``2`` +**unit**: ``seconds`` +============ =========== -Configure timeout to wait for an authentication response when establishing a SSH connection. +Configure timeout to wait for an authentication response when establishing a SSH +connection. ``OPENWISP_SSH_BANNER_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``60`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ +============ =========== +**type**: ``int`` +**default**: ``60`` +**unit**: ``seconds`` +============ =========== -Configure timeout to wait for the banner to be presented when establishing a SSH connection. +Configure timeout to wait for the banner to be presented when establishing a SSH +connection. ``OPENWISP_SSH_COMMAND_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------- -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``30`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ +============ =========== +**type**: ``int`` +**default**: ``30`` +**unit**: ``seconds`` +============ =========== -Configure timeout on blocking read/write operations when executing a command in a SSH connection. +Configure timeout on blocking read/write operations when executing a command in a SSH +connection. ``OPENWISP_SSH_CONNECTION_TIMEOUT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- -+--------------+-------------+ -| **type**: | ``int`` | -+--------------+-------------+ -| **default**: | ``5`` | -+--------------+-------------+ -| **unit**: | ``seconds`` | -+--------------+-------------+ +============ =========== +**type**: ``int`` +**default**: ``5`` +**unit**: ``seconds`` +============ =========== Configure timeout for the TCP connect when establishing a SSH connection. ``OPENWISP_CONNECTORS`` -~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+------------------------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.connection.connectors.ssh.Ssh', 'SSH'), | -| | ('openwisp_controller.connection.connectors.openwrt.snmp.OpenWRTSnmp', 'OpenWRT SNMP'), | -| | ('openwisp_controller.connection.connectors.airos.snmp.AirOsSnmp', 'Ubiquiti AirOS SNMP'), | -| | ) | -+--------------+------------------------------------------------------------------------------------------------+ - -Available connector classes. Connectors are python classes that specify ways -in which OpenWISP can connect to devices in order to launch commands. +----------------------- + +============ ================================================================================= +**type**: ``tuple`` +**default**: .. code-block:: python + + ( + ("openwisp_controller.connection.connectors.ssh.Ssh", "SSH"), + ( + "openwisp_controller.connection.connectors.openwrt.snmp.OpenWRTSnmp", + "OpenWRT SNMP", + ), + ( + "openwisp_controller.connection.connectors.airos.snmp.AirOsSnmp", + "Ubiquiti AirOS SNMP", + ), + ) +============ ================================================================================= + +Available connector classes. Connectors are python classes that specify ways in which +OpenWISP can connect to devices in order to launch commands. ``OPENWISP_UPDATE_STRATEGIES`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------ -+--------------+----------------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+----------------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt', 'OpenWRT SSH'), | -| | ) | -+--------------+----------------------------------------------------------------------------------------+ +============ ======================================================================================= +**type**: ``tuple`` +**default**: .. code-block:: python -Available update strategies. An update strategy is a subclass of a -connector class which defines an ``update_config`` method which is -in charge of updating the configuration of the device. + (("openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt", "OpenWRT SSH"),) +============ ======================================================================================= -This operation is launched in a background worker when the configuration -of a device is changed. +Available update strategies. An update strategy is a subclass of a connector class which +defines an ``update_config`` method which is in charge of updating the configuration of +the device. -It's possible to write custom update strategies and add them to this -setting to make them available in OpenWISP. +This operation is launched in a background worker when the configuration of a device is +changed. + +It's possible to write custom update strategies and add them to this setting to make +them available in OpenWISP. ``OPENWISP_CONFIG_UPDATE_MAPPING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | { | -| | 'netjsonconfig.OpenWrt': OPENWISP_UPDATE_STRATEGIES[0][0], | -| | } | -+--------------+--------------------------------------------------------------------+ +---------------------------------- + +============ ================================================================== +**type**: ``dict`` +**default**: .. code-block:: python + + { + "netjsonconfig.OpenWrt": OPENWISP_UPDATE_STRATEGIES[0][0], + } +============ ================================================================== A dictionary that maps configuration backends to update strategies in order to -automatically determine the update strategy of a device connection if the -update strategy field is left blank by the user. +automatically determine the update strategy of a device connection if the update +strategy field is left blank by the user. -.. _OPENWISP_CONTROLLER_BACKENDS: +.. _openwisp_controller_backends: ``OPENWISP_CONTROLLER_BACKENDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+-----------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+-----------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('netjsonconfig.OpenWrt', 'OpenWRT'), | -| | ('netjsonconfig.OpenWisp', 'OpenWISP'), | -| | ) | -+--------------+-----------------------------------------------+ +-------------------------------- + +============ =============================================== +**type**: ``tuple`` +**default**: .. code-block:: python + + ( + ("netjsonconfig.OpenWrt", "OpenWRT"), + ("netjsonconfig.OpenWisp", "OpenWISP"), + ) +============ =============================================== Available configuration backends. For more information, see `netjsonconfig backends `_. ``OPENWISP_CONTROLLER_VPN_BACKENDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+----------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+----------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.vpn_backends.OpenVpn', 'OpenVPN'), | -| | ('openwisp_controller.vpn_backends.Wireguard', 'WireGuard'), | -| | ('openwisp_controller.vpn_backends.VxlanWireguard', 'VXLAN over WireGuard'), | -| | ('openwisp_controller.vpn_backends.ZeroTier', 'ZeroTier'), | -| | ) | -+--------------+----------------------------------------------------------------------------------+ - -Available VPN backends for VPN Server objects. For more information, see `netjsonconfig VPN backends +------------------------------------ + +============ ==================================================================================== +**type**: ``tuple`` +**default**: .. code-block:: python + + ( + ("openwisp_controller.vpn_backends.OpenVpn", "OpenVPN"), + ("openwisp_controller.vpn_backends.Wireguard", "WireGuard"), + ("openwisp_controller.vpn_backends.VxlanWireguard", "VXLAN over WireGuard"), + ("openwisp_controller.vpn_backends.ZeroTier", "ZeroTier"), + ) +============ ==================================================================================== + +Available VPN backends for VPN Server objects. For more information, see `netjsonconfig +VPN backends `_. -A VPN backend must follow some basic rules in order to be compatible with *openwisp-controller*: +A VPN backend must follow some basic rules in order to be compatible with +*openwisp-controller*: -* it MUST allow at minimum and at maximum one VPN instance -* the main *NetJSON* property MUST match the lowercase version of the class name, - eg: when using the ``OpenVpn`` backend, the system will look into - ``config['openvpn']`` -* it SHOULD focus on the server capabilities of the VPN software being used +- it MUST allow at minimum and at maximum one VPN instance +- the main *NetJSON* property MUST match the lowercase version of the class name, eg: + when using the ``OpenVpn`` backend, the system will look into ``config['openvpn']`` +- it SHOULD focus on the server capabilities of the VPN software being used -.. _OPENWISP_CONTROLLER_DEFAULT_BACKEND: +.. _openwisp_controller_default_backend: ``OPENWISP_CONTROLLER_DEFAULT_BACKEND`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------- -+--------------+----------------------------------------+ -| **type**: | ``str`` | -+--------------+----------------------------------------+ -| **default**: | ``OPENWISP_CONTROLLER_BACKENDS[0][0]`` | -+--------------+----------------------------------------+ +============ ====================================== +**type**: ``str`` +**default**: ``OPENWISP_CONTROLLER_BACKENDS[0][0]`` +============ ====================================== The preferred backend that will be used as initial value when adding new ``Config`` or ``Template`` objects in the admin. -This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_BACKENDS`` setting, -which is ``netjsonconfig.OpenWrt``. +This setting defaults to the raw value of the first item in the +``OPENWISP_CONTROLLER_BACKENDS`` setting, which is ``netjsonconfig.OpenWrt``. Setting it to ``None`` will force the user to choose explicitly. ``OPENWISP_CONTROLLER_DEFAULT_VPN_BACKEND`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+--------------------------------------------+ -| **type**: | ``str`` | -+--------------+--------------------------------------------+ -| **default**: | ``OPENWISP_CONTROLLER_VPN_BACKENDS[0][0]`` | -+--------------+--------------------------------------------+ +============ ========================================== +**type**: ``str`` +**default**: ``OPENWISP_CONTROLLER_VPN_BACKENDS[0][0]`` +============ ========================================== -The preferred backend that will be used as initial value when adding new ``Vpn`` objects in the admin. +The preferred backend that will be used as initial value when adding new ``Vpn`` objects +in the admin. -This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_VPN_BACKENDS`` setting, -which is ``openwisp_controller.vpn_backends.OpenVpn``. +This setting defaults to the raw value of the first item in the +``OPENWISP_CONTROLLER_VPN_BACKENDS`` setting, which is +``openwisp_controller.vpn_backends.OpenVpn``. Setting it to ``None`` will force the user to choose explicitly. ``OPENWISP_CONTROLLER_REGISTRATION_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== Whether devices can automatically register through the controller or not. This feature is enabled by default. -Autoregistration must be supported on the devices in order to work, see `openwisp-config automatic -registration `_ for more information. +Autoregistration must be supported on the devices in order to work, see `openwisp-config +automatic registration +`_ for more +information. ``OPENWISP_CONTROLLER_CONSISTENT_REGISTRATION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -Whether devices that are already registered are recognized when reflashed or reset, hence keeping -the existing configuration without creating a new one. +Whether devices that are already registered are recognized when reflashed or reset, +hence keeping the existing configuration without creating a new one. This feature is enabled by default. -Autoregistration must be enabled also on the devices in order to work, see `openwisp-config -consistent key generation `_ -for more information. +Autoregistration must be enabled also on the devices in order to work, see +`openwisp-config consistent key generation +`_ for more +information. ``OPENWISP_CONTROLLER_REGISTRATION_SELF_CREATION`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -Whether devices that are not already present in the system are allowed to register or not. +Whether devices that are not already present in the system are allowed to register or +not. -Turn this off if you still want to use auto-registration to avoid having to -manually set the device UUID and key in its configuration file but also want -to avoid indiscriminate registration of new devices without explicit permission. +Turn this off if you still want to use auto-registration to avoid having to manually set +the device UUID and key in its configuration file but also want to avoid indiscriminate +registration of new devices without explicit permission. .. _context_setting: ``OPENWISP_CONTROLLER_CONTEXT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- -+--------------+------------------+ -| **type**: | ``dict`` | -+--------------+------------------+ -| **default**: | ``{}`` | -+--------------+------------------+ +============ ======== +**type**: ``dict`` +**default**: ``{}`` +============ ======== Additional context that is passed to the default context of each device object. -``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide configuration variables. +``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide configuration +variables. -For more information regarding how to use configuration variables in OpenWISP, -refer to :doc:`variables`. +For more information regarding how to use configuration variables in OpenWISP, refer to +:doc:`variables`. -For technical information about how variables are handled in the lower levels -of OpenWISP, see `netjsonconfig context: configuration variables +For technical information about how variables are handled in the lower levels of +OpenWISP, see `netjsonconfig context: configuration variables `_. ``OPENWISP_CONTROLLER_DEFAULT_AUTO_CERT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------- -+--------------+---------------------------+ -| **type**: | ``bool`` | -+--------------+---------------------------+ -| **default**: | ``True`` | -+--------------+---------------------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== The default value of the ``auto_cert`` field for new ``Template`` objects. -The ``auto_cert`` field is valid only for templates which have ``type`` -set to ``VPN`` and indicates whether configuration regarding the VPN tunnel is -provisioned automatically to each device using the template, eg: +The ``auto_cert`` field is valid only for templates which have ``type`` set to ``VPN`` +and indicates whether configuration regarding the VPN tunnel is provisioned +automatically to each device using the template, eg: - when using OpenVPN, new `x509 `_ certificates will be generated automatically using the same CA assigned to the related VPN object -- when using WireGuard, new pair of private and public keys - (using `Curve25519 `_) will be generated, as well as - an IP address of the subnet assigned to the related VPN object -- when using `VXLAN `_ tunnels over Wireguad, - in addition to the configuration generated for WireGuard, a new VID will be generated +- when using WireGuard, new pair of private and public keys (using `Curve25519 + `_) will be generated, as well as an IP address of the + subnet assigned to the related VPN object +- when using `VXLAN `_ tunnels over Wireguad, in + addition to the configuration generated for WireGuard, a new VID will be generated automatically for each device if the configuration option "auto VNI" is turned on in the VPN object -All these auto generated configuration options will be available as -template variables. +All these auto generated configuration options will be available as template variables. -The objects that are automatically created will also be removed when they are not -needed anymore (eg: when the VPN template is removed from a configuration object). +The objects that are automatically created will also be removed when they are not needed +anymore (eg: when the VPN template is removed from a configuration object). ``OPENWISP_CONTROLLER_CERT_PATH`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- -+--------------+---------------------------+ -| **type**: | ``str`` | -+--------------+---------------------------+ -| **default**: | ``/etc/x509`` | -+--------------+---------------------------+ +============ ============= +**type**: ``str`` +**default**: ``/etc/x509`` +============ ============= -The filesystem path where x509 certificate will be installed when -downloaded on routers when ``auto_cert`` is being used (enabled by default). +The filesystem path where x509 certificate will be installed when downloaded on routers +when ``auto_cert`` is being used (enabled by default). ``OPENWISP_CONTROLLER_COMMON_NAME_FORMAT`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ -+--------------+------------------------------+ -| **type**: | ``str`` | -+--------------+------------------------------+ -| **default**: | ``{mac_address}-{name}`` | -+--------------+------------------------------+ +============ ======================== +**type**: ``str`` +**default**: ``{mac_address}-{name}`` +============ ======================== -Defines the format of the ``common_name`` attribute of VPN client certificates -that are automatically created when using VPN templates which have ``auto_cert`` -set to ``True``. A unique slug generated using `shortuuid `_ -is appended to the common name to introduce uniqueness. Therefore, resulting -common names will have ``{OPENWISP_CONTROLLER_COMMON_NAME_FORMAT}-{unique-slug}`` -format. +Defines the format of the ``common_name`` attribute of VPN client certificates that are +automatically created when using VPN templates which have ``auto_cert`` set to ``True``. +A unique slug generated using `shortuuid `_ +is appended to the common name to introduce uniqueness. Therefore, resulting common +names will have ``{OPENWISP_CONTROLLER_COMMON_NAME_FORMAT}-{unique-slug}`` format. -**Note:** If the ``name`` and ``mac address`` of the device are equal, -the ``name`` of the device will be omitted from the common name to avoid redundancy. +**Note:** If the ``name`` and ``mac address`` of the device are equal, the ``name`` of +the device will be omitted from the common name to avoid redundancy. ``OPENWISP_CONTROLLER_MANAGEMENT_IP_DEVICE_LIST`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------- -+--------------+------------------------------+ -| **type**: | ``bool`` | -+--------------+------------------------------+ -| **default**: | ``True`` | -+--------------+------------------------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -In the device list page, the column ``IP`` will show the ``management_ip`` if -available, defaulting to ``last_ip`` otherwise. +In the device list page, the column ``IP`` will show the ``management_ip`` if available, +defaulting to ``last_ip`` otherwise. -If this setting is set to ``False`` the ``management_ip`` won't be shown -in the device list page even if present, it will be shown only in the device -detail page. +If this setting is set to ``False`` the ``management_ip`` won't be shown in the device +list page even if present, it will be shown only in the device detail page. -You may set this to ``False`` if for some reason the majority of your user -doesn't care about the management ip address. +You may set this to ``False`` if for some reason the majority of your user doesn't care +about the management ip address. ``OPENWISP_CONTROLLER_CONFIG_BACKEND_FIELD_SHOWN`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------- -+--------------+------------------------------+ -| **type**: | ``bool`` | -+--------------+------------------------------+ -| **default**: | ``True`` | -+--------------+------------------------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -This setting toggles the ``backend`` fields in add/edit pages in Device and Template configuration, -as well as the ``backend`` field/filter in Device list and Template list. +This setting toggles the ``backend`` fields in add/edit pages in Device and Template +configuration, as well as the ``backend`` field/filter in Device list and Template list. If this setting is set to ``False`` these items will be removed from the UI. Note: This setting affects only the configuration backend and NOT the VPN backend. ``OPENWISP_CONTROLLER_DEVICE_NAME_UNIQUE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -This setting conditionally enforces unique Device names in an Organization. -The query to enforce this is case-insensitive. +This setting conditionally enforces unique Device names in an Organization. The query to +enforce this is case-insensitive. -Note: For this constraint to be optional, it is enforced on an application level and not on database. +Note: For this constraint to be optional, it is enforced on an application level and not +on database. ``OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``False`` | -+--------------+-------------+ +============ ========= +**type**: ``bool`` +**default**: ``False`` +============ ========= -The field ``hardware_id`` can be used to store a unique hardware id, for example a serial number. +The field ``hardware_id`` can be used to store a unique hardware id, for example a +serial number. -If this setting is set to ``True`` then this field will be shown first in the device list page -and in the add/edit device page. +If this setting is set to ``True`` then this field will be shown first in the device +list page and in the add/edit device page. This feature is disabled by default. ``OPENWISP_CONTROLLER_HARDWARE_ID_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+--------------------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+--------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | { | -| | 'blank': not OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED, | -| | 'null': True, | -| | 'max_length': 32, | -| | 'unique': True, | -| | 'verbose_name': _('Serial number'), | -| | 'help_text': _('Serial number of this device') | -| | } | -+--------------+--------------------------------------------------------------+ +------------------------------------------- + +============ ============================================================= +**type**: ``dict`` +**default**: .. code-block:: python + + { + "blank": not OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED, + "null": True, + "max_length": 32, + "unique": True, + "verbose_name": _("Serial number"), + "help_text": _("Serial number of this device"), + } +============ ============================================================= Options for the model field ``hardware_id``. -* ``blank``: wether the field is allowed to be blank -* ``null``: wether an empty value will be stored as ``NULL`` in the database -* ``max_length``: maximum length of the field -* ``unique``: wether the value of the field must be unique -* ``verbose_name``: text for the human readable label of the field -* ``help_text``: help text to be displayed with the field +- ``blank``: wether the field is allowed to be blank +- ``null``: wether an empty value will be stored as ``NULL`` in the database +- ``max_length``: maximum length of the field +- ``unique``: wether the value of the field must be unique +- ``verbose_name``: text for the human readable label of the field +- ``help_text``: help text to be displayed with the field ``OPENWISP_CONTROLLER_HARDWARE_ID_AS_NAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -When the hardware ID feature is enabled, devices will be referenced with -their hardware ID instead of their name. +When the hardware ID feature is enabled, devices will be referenced with their hardware +ID instead of their name. If you still want to reference devices by their name, set this to ``False``. ``OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+----------------------------+ -| **type**: | ``tuple`` | -+--------------+----------------------------+ -| **default**: | ``('Device', 'Devices')`` | -+--------------+----------------------------+ +============ ========================= +**type**: ``tuple`` +**default**: ``('Device', 'Devices')`` +============ ========================= -Defines the ``verbose_name`` attribute of the ``Device`` model, which is displayed in the -admin site. The first and second element of the tuple represent the singular and plural forms. +Defines the ``verbose_name`` attribute of the ``Device`` model, which is displayed in +the admin site. The first and second element of the tuple represent the singular and +plural forms. For example, if we want to change the verbose name to "Hotspot", we could write: .. code-block:: python - OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME = ('Hotspot', 'Hotspots') + OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME = ("Hotspot", "Hotspots") ``OPENWISP_CONTROLLER_HIDE_AUTOMATICALLY_GENERATED_SUBNETS_AND_IPS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------------------------- -+--------------+-----------+ -| **type**: | ``bool`` | -+--------------+-----------+ -| **default**: | ``False`` | -+--------------+-----------+ +============ ========= +**type**: ``bool`` +**default**: ``False`` +============ ========= -Setting this to ``True`` will hide subnets and IP addresses generated -by :doc:`subnet division rules ` -from being displayed in the list of Subnets and IP addresses in the -admin dashboard. +Setting this to ``True`` will hide subnets and IP addresses generated by :doc:`subnet +division rules ` from being displayed in the list of Subnets and +IP addresses in the admin dashboard. -.. _OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES: +.. _openwisp_controller_subnet_division_types: ``OPENWISP_CONTROLLER_SUBNET_DIVISION_TYPES`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+---------------------------------------------------------------------------------------------------------+ -| **type**: | ``tuple`` | -+--------------+---------------------------------------------------------------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | ( | -| | ('openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType', 'Device'), | -| | ('openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType', 'VPN'), | -| | ) | -| | | -+--------------+---------------------------------------------------------------------------------------------------------+ - -Available types for -:doc:`Subject Division Rule ` objects. +--------------------------------------------- + +============ ================================================================================================= +**type**: ``tuple`` +**default**: .. code-block:: python + + ( + ( + "openwisp_controller.subnet_division.rule_types.device.DeviceSubnetDivisionRuleType", + "Device", + ), + ( + "openwisp_controller.subnet_division.rule_types.vpn.VpnSubnetDivisionRuleType", + "VPN", + ), + ) +============ ================================================================================================= + +Available types for :doc:`Subject Division Rule ` objects. For more information on how to write your own types, please refer to: :ref:`custom_subnet_division_rule_types`. ``OPENWISP_CONTROLLER_API`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- -+--------------+-----------+ -| **type**: | ``bool`` | -+--------------+-----------+ -| **default**: | ``True`` | -+--------------+-----------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -Indicates whether the API for Openwisp Controller is enabled or not. -To disable the API by default add ``OPENWISP_CONTROLLER_API = False`` -in your project ``settings.py`` file. +Indicates whether the API for Openwisp Controller is enabled or not. To disable the API +by default add ``OPENWISP_CONTROLLER_API = False`` in your project ``settings.py`` file. ``OPENWISP_CONTROLLER_API_HOST`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------- -+--------------+-----------+ -| **type**: | ``str`` | -+--------------+-----------+ -| **default**: | ``None`` | -+--------------+-----------+ +============ ======== +**type**: ``str`` +**default**: ``None`` +============ ======== Allows to specify backend URL for API requests, if the frontend is hosted separately. -.. _OPENWISP_CONTROLLER_USER_COMMANDS: +.. _openwisp_controller_user_commands: ``OPENWISP_CONTROLLER_USER_COMMANDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------- -+--------------+----------+ -| **type**: | ``list`` | -+--------------+----------+ -| **default**: | ``[]`` | -+--------------+----------+ +============ ======== +**type**: ``list`` +**default**: ``[]`` +============ ======== -Allows to specify a ``list`` of tuples for adding commands -as described in the section: :ref:`defining_new_menu_options`. +Allows to specify a ``list`` of tuples for adding commands as described in the section: +:ref:`defining_new_menu_options`. ``OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -+--------------+------------------------------------------------+ -| **type**: | ``dict`` | -+--------------+------------------------------------------------+ -| **default**: | .. code-block:: python | -| | | -| | { | -| | # By default all commands are allowed | -| | '__all__': '*', | -| | } | -| | | -+--------------+------------------------------------------------+ - -This setting controls the command types that are enabled on the system -By default, all command types are enabled to all the organizations, -but it's possible to disable a specific command for a specific organization -as shown in the following example: +----------------------------------------------------- + +============ ============================================= +**type**: ``dict`` +**default**: .. code-block:: python + + { + # By default all commands are allowed + "__all__": "*", + } +============ ============================================= + +This setting controls the command types that are enabled on the system By default, all +command types are enabled to all the organizations, but it's possible to disable a +specific command for a specific organization as shown in the following example: .. code-block:: python OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS = { - '__all__': '*', + "__all__": "*", # Organization UUID: # Tuple of enabled commands - '7448a190-6e65-42bf-b8ea-bb6603e593a5': ('reboot', 'change_password'), + "7448a190-6e65-42bf-b8ea-bb6603e593a5": ("reboot", "change_password"), } -In the example above, the organization with UUID ``7448a190-6e65-42bf-b8ea-bb6603e593a5`` -will allow to send only commands of type ``reboot`` and ``change_password``, -while all the other organizations will have all command types enabled. +In the example above, the organization with UUID +``7448a190-6e65-42bf-b8ea-bb6603e593a5`` will allow to send only commands of type +``reboot`` and ``change_password``, while all the other organizations will have all +command types enabled. -.. _OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA: +.. _openwisp_controller_device_group_schema: ``OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------- -+--------------+------------------------------------------+ -| **type**: | ``dict`` | -+--------------+------------------------------------------+ -| **default**: | ``{'type': 'object', 'properties': {}}`` | -+--------------+------------------------------------------+ +============ ======================================== +**type**: ``dict`` +**default**: ``{'type': 'object', 'properties': {}}`` +============ ======================================== -Allows specifying JSONSchema used for -validating the meta-data of :doc:`device-groups`. +Allows specifying JSONSchema used for validating the meta-data of :doc:`device-groups`. ``OPENWISP_CONTROLLER_SHARED_MANAGEMENT_IP_ADDRESS_SPACE`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------------------- -+--------------+----------+ -| **type**: | ``bool`` | -+--------------+----------+ -| **default**: | ``True`` | -+--------------+----------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -By default, the system assumes that the address space of the management -tunnel is shared among all the organizations using the system, that is, -the system assumes there's only one management VPN, tunnel or other -networking technology to reach the devices it controls. +By default, the system assumes that the address space of the management tunnel is shared +among all the organizations using the system, that is, the system assumes there's only +one management VPN, tunnel or other networking technology to reach the devices it +controls. -When set to ``True``, any device belonging to any -organization will never have the same ``management_ip`` as another device, -the latest device declaring the management IP will take the IP and any -other device who declared the same IP in the past will have the field -reset to empty state to avoid potential conflicts. +When set to ``True``, any device belonging to any organization will never have the same +``management_ip`` as another device, the latest device declaring the management IP will +take the IP and any other device who declared the same IP in the past will have the +field reset to empty state to avoid potential conflicts. -Set this to ``False`` if every organization has its dedicated management -tunnel with a dedicated address space that is reachable by the OpenWISP server. +Set this to ``False`` if every organization has its dedicated management tunnel with a +dedicated address space that is reachable by the OpenWISP server. ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ -+--------------+-------------+ -| **type**: | ``bool`` | -+--------------+-------------+ -| **default**: | ``True`` | -+--------------+-------------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -By default, only the management IP will be used to establish connection -with the devices. +By default, only the management IP will be used to establish connection with 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``. ``OPENWISP_CONTROLLER_DSA_OS_MAPPING`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------- -+--------------+----------+ -| **type**: | ``dict`` | -+--------------+----------+ -| **default**: | ``{}`` | -+--------------+----------+ +============ ======== +**type**: ``dict`` +**default**: ``{}`` +============ ======== -OpenWISP Controller can figure out whether it should use the new OpenWrt syntax -for DSA interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by -reading the ``os`` field of the ``Device`` object. However, if the firmware you -are using has a custom firmware identifier, the system will not be able to figure -out whether it should use the new syntax and it will default to -:ref:`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK `. +OpenWISP Controller can figure out whether it should use the new OpenWrt syntax for DSA +interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by reading the +``os`` field of the ``Device`` object. However, if the firmware you are using has a +custom firmware identifier, the system will not be able to figure out whether it should +use the new syntax and it will default to :ref:`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK +`. -If you want to make sure the system can parse your custom firmware -identifier properly, you can follow the example below. +If you want to make sure the system can parse your custom firmware identifier properly, +you can follow the example below. -For the sake of the example, the OS identifier ``MyCustomFirmware 2.0`` -corresponds to ``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` corresponds to -``OpenWrt 21.02``. Configuring this setting as indicated below will allow -OpenWISP to supply the right syntax automatically. +For the sake of the example, the OS identifier ``MyCustomFirmware 2.0`` corresponds to +``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` corresponds to ``OpenWrt 21.02``. +Configuring this setting as indicated below will allow OpenWISP to supply the right +syntax automatically. Example: .. code-block:: python OPENWISP_CONTROLLER_DSA_OS_MAPPING = { - 'netjsonconfig.OpenWrt': { + "netjsonconfig.OpenWrt": { # OpenWrt >=21.02 configuration syntax will be used for # these OS identifiers. - '>=21.02': [r'MyCustomFirmware 2.1(.*)'], + ">=21.02": [r"MyCustomFirmware 2.1(.*)"], # OpenWrt <=21.02 configuration syntax will be used for # these OS identifiers. - '<21.02': [r'MyCustomFirmware 2.0(.*)'] + "<21.02": [r"MyCustomFirmware 2.0(.*)"], } } **Note**: The OS identifier should be a regular expression as shown in above example. -.. _OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK: +.. _openwisp_controller_dsa_default_fallback: ``OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- -+--------------+----------+ -| **type**: | ``bool`` | -+--------------+----------+ -| **default**: | ``True`` | -+--------------+----------+ +============ ======== +**type**: ``bool`` +**default**: ``True`` +============ ======== -The value of this setting decides whether to use DSA syntax -(OpenWrt >=21 configuration syntax) if openwisp-controller fails -to make that decision automatically. +The value of this setting decides whether to use DSA syntax (OpenWrt >=21 configuration +syntax) if openwisp-controller fails to make that decision automatically. ``OPENWISP_CONTROLLER_GROUP_PIE_CHART`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------- -+--------------+-----------+ -| **type**: | ``bool`` | -+--------------+-----------+ -| **default**: | ``False`` | -+--------------+-----------+ +============ ========= +**type**: ``bool`` +**default**: ``False`` +============ ========= Allows to show a pie chart like the one in the screenshot. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/devicegroups-piechart.png - :alt: device groups piechart + :alt: device groups piechart -Active groups are groups which have at least one device in them, -while emtpy groups do not have any device assigned. +Active groups are groups which have at least one device in them, while emtpy groups do +not have any device assigned. ``OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------- -+--------------+-----------+ -| **type**: | ``dict`` | -+--------------+-----------+ -| **default**: | see below | -+--------------+-----------+ +============ ========= +**type**: ``dict`` +**default**: see below +============ ========= .. code-block:: python # default value of OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS: dict( - max_retries=5, # total number of retries - retry_backoff=True, # exponential backoff - retry_backoff_max=600, # 10 minutes - retry_jitter=True, # randomness into exponential backoff + max_retries=5, # total number of retries + retry_backoff=True, # exponential backoff + retry_backoff_max=600, # 10 minutes + retry_jitter=True, # randomness into exponential backoff ) +This setting is utilized by background API tasks executed by :doc:`ZeroTier VPN servers +and ZeroTier VPN clients ` to handle recoverable HTTP status codes such as +429, 500, 502, 503, and 504. -This setting is utilized by background API tasks executed -by :doc:`ZeroTier VPN servers and ZeroTier VPN clients ` -to handle recoverable HTTP status codes such as 429, 500, 502, 503, -and 504. - -These tasks are retried with a maximum of 5 attempts with an -exponential backoff and jitter, with a maximum delay of 10 minutes. +These tasks are retried with a maximum of 5 attempts with an exponential backoff and +jitter, with a maximum delay of 10 minutes. -This feature ensures that ZeroTier Service API calls -are resilient to recoverable failures, improving the -reliability of the system. +This feature ensures that ZeroTier Service API calls are resilient to recoverable +failures, improving the reliability of the system. -For more information on these settings, you can refer to the -`the celery documentation regarding automatic retries -for known errors. +For more information on these settings, you can refer to the `the celery documentation +regarding automatic retries for known errors. `_ diff --git a/docs/user/shell-commands.rst b/docs/user/shell-commands.rst index 79f1aa7da..d64e3accc 100644 --- a/docs/user/shell-commands.rst +++ b/docs/user/shell-commands.rst @@ -7,38 +7,36 @@ By default, there are three options in the **Send Command** dropdown: 2. Change Password 3. Custom Command -While the first two options are self-explanatory, the **custom command** option -allows you to execute any command on the device as shown in the example below. +While the first two options are self-explanatory, the **custom command** option allows +you to execute any command on the device as shown in the example below. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/commands_demo.gif - :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/commands_demo.gif - :alt: Executing commands on device example + :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/commands_demo.gif + :alt: Executing commands on device example -**Note**: in order for this feature to work, a device needs to have at least -one **Access Credential** (see -:doc:`How to configure push updates `). +**Note**: in order for this feature to work, a device needs to have at least one +**Access Credential** (see :doc:`How to configure push updates `). -The **Send Command** button will be hidden until the device -has at least one **Access Credential**. +The **Send Command** button will be hidden until the device has at least one **Access +Credential**. -If you need to allow your users to quickly send specific commands that are used often in your -network regardless of your users' knowledge of Linux shell commands, you can add new commands -by following instructions in the :ref:`defining_new_menu_options` section below. +If you need to allow your users to quickly send specific commands that are used often in +your network regardless of your users' knowledge of Linux shell commands, you can add +new commands by following instructions in the :ref:`defining_new_menu_options` section +below. .. note:: - If you're an advanced user and want to learn how to register - commands programmatically, refer to the - :ref:`registering_unregistering_commands` section. + If you're an advanced user and want to learn how to register commands + programmatically, refer to the :ref:`registering_unregistering_commands` section. .. _defining_new_menu_options: Defining New Options in the Commands Menu ----------------------------------------- -Let's explore to define new custom commands -to help users perform additional management actions -without having to be Linux/Unix experts. +Let's explore to define new custom commands to help users perform additional management +actions without having to be Linux/Unix experts. We can do so by using the ``OPENWISP_CONTROLLER_USER_COMMANDS`` django setting. @@ -49,50 +47,52 @@ The following example defines a simple command that can ``ping`` an input # In yourproject/settings.py + def ping_command_callable(destination_address, interface_name=None): - command = f'ping -c 4 {destination_address}' + command = f"ping -c 4 {destination_address}" if interface_name: - command += f' -I {interface_name}' + command += f" -I {interface_name}" return command + OPENWISP_CONTROLLER_USER_COMMANDS = [ ( - 'ping', + "ping", { - 'label': 'Ping', - 'schema': { - 'title': 'Ping', - 'type': 'object', - 'required': ['destination_address'], - 'properties': { - 'destination_address': { - 'type': 'string', - 'title': 'Destination Address', + "label": "Ping", + "schema": { + "title": "Ping", + "type": "object", + "required": ["destination_address"], + "properties": { + "destination_address": { + "type": "string", + "title": "Destination Address", }, - 'interface_name': { - 'type': 'string', - 'title': 'Interface Name', + "interface_name": { + "type": "string", + "title": "Interface Name", }, }, - 'message': 'Destination Address cannot be empty', - 'additionalProperties': False, + "message": "Destination Address cannot be empty", + "additionalProperties": False, }, - 'callable': ping_command_callable, - } + "callable": ping_command_callable, + }, ) ] -The above code will add the *Ping* command in the user interface as show -in the GIF below: +The above code will add the *Ping* command in the user interface as show in the GIF +below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/ping_command_example.gif - :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/ping_command_example.gif - :alt: Adding a *ping* command + :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/ping_command_example.gif + :alt: Adding a *ping* command -The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of ``tuple`` -each containing two elements. The first element of the tuple should contain an -identifier for the command and the second element should contain a ``dict`` -defining configuration of the command. +The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of ``tuple`` each +containing two elements. The first element of the tuple should contain an identifier for +the command and the second element should contain a ``dict`` defining configuration of +the command. .. _comand_configuration: @@ -102,16 +102,16 @@ Command Configuration The ``dict`` defining configuration for command should contain following keys: 1. ``label`` -^^^^^^^^^^^^ +++++++++++++ A ``str`` defining label for the command used internally by Django. 2. ``schema`` -^^^^^^^^^^^^^ ++++++++++++++ -A ``dict`` defining `JSONSchema `_ for inputs of command. -You can specify the inputs for your command, add rules for performing validation -and make inputs required or optional. +A ``dict`` defining `JSONSchema `_ for inputs of command. You +can specify the inputs for your command, add rules for performing validation and make +inputs required or optional. Here is a detailed explanation of the schema used in above example: @@ -119,47 +119,44 @@ Here is a detailed explanation of the schema used in above example: { # Name of the command displayed in *Send Command* widget - 'title': 'Ping', + "title": "Ping", # Use type *object* if the command needs to accept inputs # Use type *null* if the command does not accepts any input - 'type': 'object', + "type": "object", # Specify list of inputs that are required - 'required': ['destination_address'], + "required": ["destination_address"], # Define the inputs for the commands along with their properties - 'properties': { - 'destination_address': { + "properties": { + "destination_address": { # type of the input value - 'type': 'string', + "type": "string", # label used for displaying this input field - 'title': 'Destination Address', + "title": "Destination Address", }, - 'interface_name': { - 'type': 'string', - 'title': 'Interface Name', + "interface_name": { + "type": "string", + "title": "Interface Name", }, }, # Error message to be shown if validation fails - 'message': 'Destination Address cannot be empty'), + "message": "Destination Address cannot be empty", # Whether specifying addtionaly inputs is allowed from the input form - 'additionalProperties': False, + "additionalProperties": False, } -This example uses only handful of properties available in JSONSchema. You can -experiment with other properties of JSONSchema for schema of your command. +This example uses only handful of properties available in JSONSchema. You can experiment +with other properties of JSONSchema for schema of your command. 3. ``callable`` -^^^^^^^^^^^^^^^ ++++++++++++++++ -A ``callable`` or ``str`` defining dotted path to a callable. It should return -the command (``str``) to be executed on the device. Inputs of the command are -passed as arguments to this callable. +A ``callable`` or ``str`` defining dotted path to a callable. It should return the +command (``str``) to be executed on the device. Inputs of the command are passed as +arguments to this callable. -The example above includes a callable(``ping_command_callable``) for -``ping`` command. +The example above includes a callable(``ping_command_callable``) for ``ping`` command. How to register or unregister commands -------------------------------------- -Refer to -:ref:`registering_unregistering_commands` -in the developer documentation. +Refer to :ref:`registering_unregistering_commands` in the developer documentation. diff --git a/docs/user/subnet-division-rules.rst b/docs/user/subnet-division-rules.rst index 13d6b5432..bad831009 100644 --- a/docs/user/subnet-division-rules.rst +++ b/docs/user/subnet-division-rules.rst @@ -1,8 +1,8 @@ Automating Subnet and IP Address Provisioning ============================================= -This guide helps you automate provisioning subnets -and IP addresses for your network devices. +This guide helps you automate provisioning subnets and IP addresses for your network +devices. .. _step1_rule: @@ -11,87 +11,83 @@ and IP addresses for your network devices. Create a master subnet. -This is the parent subnet from which automatically -generated subnets will be provisioned. +This is the parent subnet from which automatically generated subnets will be +provisioned. .. note:: - Choose a subnet size appropriate for the needs of your network. + Choose a subnet size appropriate for the needs of your network. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png - :alt: Creating a master subnet example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png + :alt: Creating a master subnet example -On the same page, add a **subnet division rule**. -This rule defines the criteria for automatically -provisioning subnets under the master subnet. +On the same page, add a **subnet division rule**. This rule defines the criteria for +automatically provisioning subnets under the master subnet. -The type of subnet division rule determines when subnets and IP addresses -are assigned to devices. +The type of subnet division rule determines when subnets and IP addresses are assigned +to devices. The currently supported rule types are described below. .. note:: - For information on how to write your own subnet division rule types, - please refer to: - :ref:`custom_subnet_division_rule_types`. + For information on how to write your own subnet division rule types, please refer + to: :ref:`custom_subnet_division_rule_types`. .. _device_rule: Device Subnet Division Rule ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This rule triggers when a device configuration (``config.Config`` model) -is created for the organization specified in the rule. +This rule triggers when a device configuration (``config.Config`` model) is created for +the organization specified in the rule. .. note:: - If a device object is created without any related - configuration object, it will not trigger this rule. + If a device object is created without any related configuration object, it will not + trigger this rule. -Creating a new *"Device"* rule will also automatically provision -subnets and IP addresses for existing devices within the organization. +Creating a new *"Device"* rule will also automatically provision subnets and IP +addresses for existing devices within the organization. .. _vpn_rule: VPN Subnet Division Rule ~~~~~~~~~~~~~~~~~~~~~~~~ -This rule triggers when a template flagged as *VPN-client* -is assigned to a device configuration, but only if the -VPN server associated with the VPN-client template -uses the same subnet to which the subnet division rule -is assignated to. +This rule triggers when a template flagged as *VPN-client* is assigned to a device +configuration, but only if the VPN server associated with the VPN-client template uses +the same subnet to which the subnet division rule is assignated to. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png - :alt: Creating a subnet division rule example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png + :alt: Creating a subnet division rule example In this example, **VPN subnet division rule** is used. 2. Create a VPN Server ---------------------- -Now create a VPN Server and choose the previously created **master subnet** as the subnet for -this VPN Server. +Now create a VPN Server and choose the previously created **master subnet** as the +subnet for this VPN Server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png - :alt: Creating a VPN Server example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png + :alt: Creating a VPN Server example 3. Create a VPN Client Template ------------------------------- -Create a template, setting the **Type** field to **VPN Client** and **VPN** field to use the -previously created VPN Server. +Create a template, setting the **Type** field to **VPN Client** and **VPN** field to use +the previously created VPN Server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png - :alt: Creating a VPN Client template example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png + :alt: Creating a VPN Client template example -**Note**: You can also check the **Enable by default** field if you want to automatically -apply this template to devices that will register in future. +**Note**: You can also check the **Enable by default** field if you want to +automatically apply this template to devices that will register in future. 4. Apply VPN Client Template to Devices --------------------------------------- @@ -99,36 +95,37 @@ apply this template to devices that will register in future. With everything in place, you can now apply the VPN Client Template to devices. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png - :alt: Adding template to device example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png + :alt: Adding template to device example After saving the device, you should see all provisioned Subnets and IPs for this device under :ref:`System Defined Variables `. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png - :alt: Provisioned Subnets and IPs available as System Defined Variables example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png + :alt: Provisioned Subnets and IPs available as System Defined Variables example -You can now use these :doc:`variables` in the configuration of devices -of your network. +You can now use these :doc:`variables` in the configuration of devices of your network. Important notes for using Subnet Division ------------------------------------------- - -- In the example provided, the Subnet, VPN Server, and VPN Client Template were associated with the **default** organization. - You can also utilize **Systemwide Shared** Subnet, VPN Server, or VPN Client Template; however, remember that - the Subnet Division Rule will always be linked to an organization. It will only be triggered when a VPN Client Template - is applied to a Device with the same organization as the Subnet Division Rule. - +----------------------------------------- + +- In the example provided, the Subnet, VPN Server, and VPN Client Template were + associated with the **default** organization. You can also utilize **Systemwide + Shared** Subnet, VPN Server, or VPN Client Template; however, remember that the Subnet + Division Rule will always be linked to an organization. It will only be triggered when + a VPN Client Template is applied to a Device with the same organization as the Subnet + Division Rule. - Configuration variables can be used for provisioned subnets and IPs in the Template. - Each variable will resolve differently for different devices. For example, ``OW_subnet1_ip1`` will resolve to - ``10.0.0.1`` for one device and ``10.0.0.55`` for another. Every device receives its own set of subnets and IPs. - Ensure to provide default fallback values in the *default values* template field (mainly used for validation). - -- The Subnet Division Rule automatically creates a reserved subnet, which can be utilized - to provision any IP addresses that need to be created manually. The remaining address space of the master subnet - must not be interfered with, or the automation implemented in this module will not function. - + Each variable will resolve differently for different devices. For example, + ``OW_subnet1_ip1`` will resolve to ``10.0.0.1`` for one device and ``10.0.0.55`` for + another. Every device receives its own set of subnets and IPs. Ensure to provide + default fallback values in the *default values* template field (mainly used for + validation). +- The Subnet Division Rule automatically creates a reserved subnet, which can be + utilized to provision any IP addresses that need to be created manually. The remaining + address space of the master subnet must not be interfered with, or the automation + implemented in this module will not function. - The example provided used the :ref:`VPN subnet division rule `. Similarly, the :ref:`device subnet division rule ` can be employed, requiring only :ref:`the creation of a subnet and a subnet division rule `. @@ -136,44 +133,41 @@ Important notes for using Subnet Division Limitations of Subnet Division Rules ------------------------------------ -In the current implementation, it is not possible to change *Size*, *Number of Subnets* and -*Number of IPs* fields of an existing subnet division rule due to following reasons: +In the current implementation, it is not possible to change *Size*, *Number of Subnets* +and *Number of IPs* fields of an existing subnet division rule due to following reasons: Size ~~~~ -Allowing to change size of provisioned subnets of an existing subnet division rule -will require rebuilding of Subnets and IP addresses which has possibility of breaking +Allowing to change size of provisioned subnets of an existing subnet division rule will +require rebuilding of Subnets and IP addresses which has possibility of breaking existing configurations. Number of Subnets ~~~~~~~~~~~~~~~~~ -Allowing to decrease number of subnets of an existing subnet division -rule can create patches of unused subnets dispersed everywhere in the master subnet. -Allowing to increase number of subnets will break the continuous allocation of subnets for -every device. It can also break configuration of devices. +Allowing to decrease number of subnets of an existing subnet division rule can create +patches of unused subnets dispersed everywhere in the master subnet. Allowing to +increase number of subnets will break the continuous allocation of subnets for every +device. It can also break configuration of devices. Number of IPs ~~~~~~~~~~~~~ -**Decreasing the number of IPs** in an existing subnet division rule -is not allowed as it can lead to deletion of IP addresses, potentially -breaking configurations of existing devices. +**Decreasing the number of IPs** in an existing subnet division rule is not allowed as +it can lead to deletion of IP addresses, potentially breaking configurations of existing +devices. **Increasing the number of IPs is allowed**. -If you need to modify any of these fields -(**Size**, **Number of Subnets**, or **Number of IPs**), we recommend -to proceed as follows: +If you need to modify any of these fields (**Size**, **Number of Subnets**, or **Number +of IPs**), we recommend to proceed as follows: 1. Delete the existing rule. 2. Create a new rule. -The automation will provision new subnets and addresses according to -the new parameters to any existing devices that are eligible to the -subnet division rule. +The automation will provision new subnets and addresses according to the new parameters +to any existing devices that are eligible to the subnet division rule. -However, be aware that existing devices **will probably -be reassigned different subnets and IP addresses** than the ones -used previously. +However, be aware that existing devices **will probably be reassigned different subnets +and IP addresses** than the ones used previously. diff --git a/docs/user/templates.rst b/docs/user/templates.rst index fc704adc5..dd7d3151d 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -1,59 +1,56 @@ Configuration Templates ======================= -Templates are designed to store configuration that can be reused by -some or all the devices in the system. +Templates are designed to store configuration that can be reused by some or all the +devices in the system. -Updating the configuration stored in a template allows to update the -configuration of all the devices that have that template assigned. +Updating the configuration stored in a template allows to update the configuration of +all the devices that have that template assigned. -This means that configuration can be defined only once for multiple -devices, and if the need to update a specific piece of configuration -arises, it can be easily achieved by updating the template. +This means that configuration can be defined only once for multiple devices, and if the +need to update a specific piece of configuration arises, it can be easily achieved by +updating the template. .. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 + :backlinks: none + :depth: 3 Template ordering and override ------------------------------ -A device can use multiple templates, **the order in which templates are -assigned to each device matters**: templates assigned last can override -templates assigned earlier, the order can be changed by drag and dropping -the template in the device configuration page as in the animated -screenshot below. +A device can use multiple templates, **the order in which templates are assigned to each +device matters**: templates assigned last can override templates assigned earlier, the +order can be changed by drag and dropping the template in the device configuration page +as in the animated screenshot below. .. image:: /images/templates/template-ordering.gif - :align: center - :alt: Template ordering: drag and drop to change order + :align: center + :alt: Template ordering: drag and drop to change order The device configuration can also override what is defined in templates. -Overriding means redefining a specific configuration section in a way that -overwrites the template. +Overriding means redefining a specific configuration section in a way that overwrites +the template. -**Overriding involves some form of duplication of information, which is -not great, it should be used as a last resort**. The recommended way to -define parts of the configuration that are specific for each device is to -use :doc:`Configuration variables <./variables>`. +**Overriding involves some form of duplication of information, which is not great, it +should be used as a last resort**. The recommended way to define parts of the +configuration that are specific for each device is to use :doc:`Configuration variables +<./variables>`. Shared templates vs organization specific ----------------------------------------- -Templates can be *organization specific* or *shared* -(no organization specified). +Templates can be *organization specific* or *shared* (no organization specified). .. image:: /images/templates/organization-specific-vs-shared.gif - :align: center - :alt: Shared templates vs organization specific + :align: center + :alt: Shared templates vs organization specific -**Organization specific templates** will be available and usable only -within the same organization which they are assigned to. +**Organization specific templates** will be available and usable only within the same +organization which they are assigned to. -If no organization is specified when creating a template, a shared -template will be created, **shared templates are available to any -organization in the system**. +If no organization is specified when creating a template, a shared template will be +created, **shared templates are available to any organization in the system**. Here are a few typical use cases of shared templates: @@ -67,31 +64,28 @@ Default Templates ----------------- .. image:: /images/templates/default-templates.gif - :align: center - :alt: Templates enabled by default + :align: center + :alt: Templates enabled by default -When templates are flagged as **"Enabled by default"**, -they will be automatically assigned to new devices. +When templates are flagged as **"Enabled by default"**, they will be automatically +assigned to new devices. -This is a very powerful feature: **once default templates are correctly -configured to implement the use case you need, you will only have to -register a device into OpenWISP for it to auto-configure itself**. +This is a very powerful feature: **once default templates are correctly configured to +implement the use case you need, you will only have to register a device into OpenWISP +for it to auto-configure itself**. -Moreover, you can change the default templates any time you need, which -is the reason this feature has replaced the practice of storing default -configuration in firmware images (which would need to be recompiled and -redistributed): with default templates, the default firmware image only -needs to contain the bare minimum configuration to connect to OpenWISP, -once the device connects to OpenWISP it will download and apply the -default templates without the need of manual intervention from -the network operators. +Moreover, you can change the default templates any time you need, which is the reason +this feature has replaced the practice of storing default configuration in firmware +images (which would need to be recompiled and redistributed): with default templates, +the default firmware image only needs to contain the bare minimum configuration to +connect to OpenWISP, once the device connects to OpenWISP it will download and apply the +default templates without the need of manual intervention from the network operators. -An organization specific template flagged as default will be automatically -assigned to any new device which will be created in the same organization. +An organization specific template flagged as default will be automatically assigned to +any new device which will be created in the same organization. -A shared default template instead will be automatically assigned to all -the new devices which will be created in the system, regardless of -organization. +A shared default template instead will be automatically assigned to all the new devices +which will be created in the system, regardless of organization. .. _required_templates: @@ -99,27 +93,25 @@ Required Templates ------------------ .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png - :alt: Required template example + :alt: Required template example -Required templates are similar to :ref:`default_templates` -but cannot be unassigned from a device configuration, -they can only be overridden. +Required templates are similar to :ref:`default_templates` but cannot be unassigned from +a device configuration, they can only be overridden. -They will be always assigned earlier than default templates, -so they can be overridden if needed. +They will be always assigned earlier than default templates, so they can be overridden +if needed. -In the example above, the "SSID" template is flagged as "(required)" -and its checkbox is always checked and disabled. +In the example above, the "SSID" template is flagged as "(required)" and its checkbox is +always checked and disabled. Device Group Templates ---------------------- -:ref:`default_templates` are an incredibly useful tool, -but they're limited: **only one set of default templates can be created** -per each organization. +:ref:`default_templates` are an incredibly useful tool, but they're limited: **only one +set of default templates can be created** per each organization. -With :ref:`device_group_templates` it is possible to specify a -set of default templates for each device group. +With :ref:`device_group_templates` it is possible to specify a set of default templates +for each device group. .. _templates_tags: @@ -127,33 +119,30 @@ Template tags ------------- .. image:: /images/templates/template-tags.gif - :align: center - :alt: Template tags + :align: center + :alt: Template tags -In some cases, you may have multiple set of default settings to use, -let's explain this with a practical example: you may have 2 different -device types in your network: +In some cases, you may have multiple set of default settings to use, let's explain this +with a practical example: you may have 2 different device types in your network: -- Mesh routers: they connect to one another, forming a - wireless mesh network -- Dumb access points: they connect to the mesh routers on the LAN - port and offer internet access which is routed via the mesh - network by the routers +- Mesh routers: they connect to one another, forming a wireless mesh network +- Dumb access points: they connect to the mesh routers on the LAN port and offer + internet access which is routed via the mesh network by the routers -In this example case, the default configuration to use in each -device type can greatly differ. +In this example case, the default configuration to use in each device type can greatly +differ. -In such a setup, default templates would only contain configuration -which is common to both device types, while configuration which is -specific for each type would be stored in specific templates which -are then tagged with specific keywords: +In such a setup, default templates would only contain configuration which is common to +both device types, while configuration which is specific for each type would be stored +in specific templates which are then tagged with specific keywords: - ``mesh``: tag to use for mesh configuration - ``dumb-ap``: tag to use for dumb AP configuration -The `openwisp-config `_ -configuration of each device type must specify the correct tag before -each device registers in the system. +The `openwisp-config +`_ configuration of +each device type must specify the correct tag before each device registers in the +system. Here's the sample ``/etc/config/openwisp`` configuration for mesh devices: @@ -164,16 +153,15 @@ Here's the sample ``/etc/config/openwisp`` configuration for mesh devices: option shared_secret 'mySharedSecret123' option tags 'mesh' -Once devices with the above configuration will register into the system, -any template tagged as ``mesh`` (as in the screenshot below) will be -assigned to them. +Once devices with the above configuration will register into the system, any template +tagged as ``mesh`` (as in the screenshot below) will be assigned to them. .. image:: /images/templates/mesh-template-tag.png - :align: center - :alt: Template tags: mesh example + :align: center + :alt: Template tags: mesh example -The sample ``/etc/config/openwisp`` configuration for dumb access -points is the following: +The sample ``/etc/config/openwisp`` configuration for dumb access points is the +following: .. code-block:: @@ -182,21 +170,19 @@ points is the following: option shared_secret 'mySharedSecret123' option tags 'dumb-ap' -Once devices with the above configuration will register into the system, -any template tagged as ``dumb-ap`` (as in the screenshot below) -will be assigned to them. +Once devices with the above configuration will register into the system, any template +tagged as ``dumb-ap`` (as in the screenshot below) will be assigned to them. .. image:: /images/templates/dumb-ap-template-tag.png - :align: center - :alt: Template tags: dumb AP example + :align: center + :alt: Template tags: dumb AP example Implementation details of templates ----------------------------------- -Templates are implemented under the hood by the OpenWISP -configuration engine: netjsonconfig. +Templates are implemented under the hood by the OpenWISP configuration engine: +netjsonconfig. -For more advanced technical information about templates, consult the -netjsonconfig documentation: -`Basic Concepts, Template +For more advanced technical information about templates, consult the netjsonconfig +documentation: `Basic Concepts, Template `_. diff --git a/docs/user/variables.rst b/docs/user/variables.rst index 9775e36ad..496a6f9f2 100644 --- a/docs/user/variables.rst +++ b/docs/user/variables.rst @@ -1,22 +1,19 @@ Configuration Variables ======================= -Sometimes the configuration is not exactly equal on all the devices, -some parameters are unique to each device or need to be changed -by the user. +Sometimes the configuration is not exactly equal on all the devices, some parameters are +unique to each device or need to be changed by the user. -In these cases it is possible to use configuration variables in -conjunction with templates, this feature is also known as -*configuration context*, think of -it like a dictionary which is passed to the function which renders the -configuration, so that it can fill variables according to the passed -context. +In these cases it is possible to use configuration variables in conjunction with +templates, this feature is also known as *configuration context*, think of it like a +dictionary which is passed to the function which renders the configuration, so that it +can fill variables according to the passed context. Different types of variables ---------------------------- -The different ways in which variables are defined are described below in -the order (high to low) of their precedence. +The different ways in which variables are defined are described below in the order (high +to low) of their precedence. .. contents:: :depth: 2 @@ -27,23 +24,22 @@ the order (high to low) of their precedence. 1. User defined device variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the device configuration section you can find a section named -"Configuration variables" where it is possible to define the configuration -variables and their values, as shown in the example below: +In the device configuration section you can find a section named "Configuration +variables" where it is possible to define the configuration variables and their values, +as shown in the example below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png - :alt: context + :alt: context 2 Predefined device variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each device gets the following attributes passed as configuration -variables: +Each device gets the following attributes passed as configuration variables: -* ``id`` -* ``key`` -* ``name`` -* ``mac_address`` +- ``id`` +- ``key`` +- ``name`` +- ``mac_address`` 3. Group variables ~~~~~~~~~~~~~~~~~~ @@ -62,58 +58,53 @@ You can set the *organization variables* from the organization change page **Configuration Management Settings**. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png - :alt: organization variables + :alt: organization variables 5. Global variables ~~~~~~~~~~~~~~~~~~~ -Variables can also be defined globally using the -:ref:`context_setting` -setting, see also -:doc:`How to Edit Django Settings <../../../../user/django-settings>`. +Variables can also be defined globally using the :ref:`context_setting` setting, see +also :doc:`How to Edit Django Settings <../../../../user/django-settings>`. 6. Template default values ~~~~~~~~~~~~~~~~~~~~~~~~~~ -It's possible to specify the default values of variables -defined in a template. +It's possible to specify the default values of variables defined in a template. This allows to achieve 2 goals: -1. pass schema validation without errors (otherwise it would not be - possible to save the template in the first place) -2. provide good default values that are valid in most cases but can be - overridden in the device if needed +1. pass schema validation without errors (otherwise it would not be possible to save the + template in the first place) +2. provide good default values that are valid in most cases but can be overridden in the + device if needed -These default values will be overridden by the -:ref:`User defined device variables `. +These default values will be overridden by the :ref:`User defined device variables +`. -The default values of variables can be manipulated from the section -"configuration variables" in the edit template page: +The default values of variables can be manipulated from the section "configuration +variables" in the edit template page: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png - :alt: default values + :alt: default values .. _system_defined_variables: 7. System defined variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Predefined device variables, global variables and other variables that -are automatically managed by the system (eg: when using templates of -type VPN-client) are displayed in the admin UI as -*System Defined Variables* in read-only mode. +Predefined device variables, global variables and other variables that are automatically +managed by the system (eg: when using templates of type VPN-client) are displayed in the +admin UI as *System Defined Variables* in read-only mode. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png - :alt: system defined variables + :alt: system defined variables Example usage of variables -------------------------- -Here's a typical use case, the WiFi SSID and WiFi password. -You don't want to define this for every device, but you may want to -allow operators to easily change the SSID or WiFi password for a -specific device without having to re-define the whole wifi interface +Here's a typical use case, the WiFi SSID and WiFi password. You don't want to define +this for every device, but you may want to allow operators to easily change the SSID or +WiFi password for a specific device without having to re-define the whole wifi interface to avoid duplicating information. This would be the template: @@ -148,9 +139,8 @@ These would be the default values in the template: "wlan0_password": "Snakeoil_pwd!321654" } -The default values can then be overridden at -:ref:`device level ` -if needed, eg: +The default values can then be overridden at :ref:`device level +` if needed, eg: .. code-block:: json @@ -162,10 +152,9 @@ if needed, eg: Implementation details of variables ----------------------------------- -Variables are implemented under the hood by the OpenWISP -configuration engine: netjsonconfig. +Variables are implemented under the hood by the OpenWISP configuration engine: +netjsonconfig. -For more advanced technical information about variables, consult the -netjsonconfig documentation: -`Basic Concepts, Context (configuration variables) +For more advanced technical information about variables, consult the netjsonconfig +documentation: `Basic Concepts, Context (configuration variables) `_. diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index c9aa1399e..17a431845 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -1,9 +1,8 @@ Automating VXLAN over WireGuard Tunnels ======================================= -By following these steps, you will be able to setup layer 2 VXLAN tunnels -encapsulated in `WireGuard `_ -tunnels which work on layer 3. +By following these steps, you will be able to setup layer 2 VXLAN tunnels encapsulated +in `WireGuard `_ tunnels which work on layer 3. .. include:: ../partials/shared-object.rst @@ -12,52 +11,51 @@ tunnels which work on layer 3. 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. 2. We will set **Name** of this VPN server ``Wireguard VXLAN`` and **Host** as - ``wireguard-vxlan-server.mydomain.com`` (update this to point to your - WireGuard VXLAN VPN server). + ``wireguard-vxlan-server.mydomain.com`` (update this to point to your WireGuard VXLAN + VPN server). 3. Select ``VXLAN over WireGuard`` from the dropdown as **VPN Backend**. 4. When using VXLAN over WireGuard, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN peer). You can create a new subnet or - select an existing one from the dropdown menu. You can also assign an - **Internal IP** to the WireGuard Server or leave it empty for OpenWISP to - configure. This IP address will be used by the WireGuard interface on - server. + (assigning an IP address to each VPN peer). You can create a new subnet or select an + existing one from the dropdown menu. You can also assign an **Internal IP** to the + WireGuard Server or leave it empty for OpenWISP to configure. This IP address will be + used by the WireGuard interface on server. 5. We have set the **Webhook Endpoint** as - ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` - for this example. You will need to update this according to you VPN upgrader - endpoint. Set **Webhook AuthToken** to any strong passphrase, this will be - used to ensure that configuration upgrades are requested from trusted - sources. + ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` for this example. + You will need to update this according to you VPN upgrader endpoint. Set **Webhook + AuthToken** to any strong passphrase, this will be used to ensure that configuration + upgrades are requested from trusted sources. - **Note**: If you are following this tutorial for also setting up WireGuard - VPN server, just substitute ``wireguard-server.mydomain.com`` with hostname - of your VPN server and follow the steps in next section. + **Note**: If you are following this tutorial for also setting up WireGuard VPN + server, just substitute ``wireguard-server.mydomain.com`` with hostname of your VPN + server and follow the steps in next section. -6. Under the configuration section, set the name of WireGuard tunnel 1 interface. - We have used ``wg0`` in this example. +6. Under the configuration section, set the name of WireGuard tunnel 1 interface. We + have used ``wg0`` in this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png - :alt: WireGuard VPN VXLAN server configuration example 1 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png + :alt: WireGuard VPN VXLAN server configuration example 1 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png - :alt: WireGuard VPN VXLAN server configuration example 2 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png + :alt: WireGuard VPN VXLAN server configuration example 2 -7. After clicking on **Save and continue editing**, you will see that OpenWISP - has automatically created public and private key for WireGuard server in - **System Defined Variables** along with internal IP address information. +7. After clicking on **Save and continue editing**, you will see that OpenWISP has + automatically created public and private key for WireGuard server in **System Defined + Variables** along with internal IP address information. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png - :alt: WireGuard VXLAN VPN server configuration example 3 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png + :alt: WireGuard VXLAN VPN server configuration example 3 2. Deploy Wireguard VXLAN VPN Server ------------------------------------ -If you haven't already set up WireGuard on your VPN server, this is a good time to do so. -We recommend using the `ansible-wireguard-openwisp `_ -role for installing WireGuard since it also installs scripts that allow OpenWISP to manage -the WireGuard VPN server along with VXLAN tunnels. +If you haven't already set up WireGuard on your VPN server, this is a good time to do +so. We recommend using the `ansible-wireguard-openwisp +`_ role for installing WireGuard +since it also installs scripts that allow OpenWISP to manage the WireGuard VPN server +along with VXLAN tunnels. Pay attention to the VPN server attributes used in your playbook. It should be the same as the VPN server configuration in OpenWISP. @@ -68,8 +66,8 @@ as the VPN server configuration in OpenWISP. 1. Visit ``/admin/config/template/add/`` to add a new template. 2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can be applied to. - For this example, we will leave it as ``OpenWRT``. +3. The **Backend** field refers to the backend of the device this template can be + applied to. For this example, we will leave it as ``OpenWRT``. 4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is ``Wireguard VXLAN``. 5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP @@ -77,32 +75,31 @@ as the VPN server configuration in OpenWISP. WireGuard VPN client along with the VXLAN Network Identifier (VNI). 6. After clicking on **Save and continue editing** button, you will see details of the *Wireguard VXLAN* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak accordingly. We will - use the automatically generated VPN client configuration for this example. + configuration will be automatically generated which you can tweak accordingly. We + will use the automatically generated VPN client configuration for this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png - :alt: WireGuard VXLAN VPN client template example - + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png + :alt: WireGuard VXLAN VPN client template example 4. Apply Wireguard VXLAN VPN template to devices ------------------------------------------------ .. note:: - This step assumes that you already have a device registered on - OpenWISP. Register or create a device before proceeding. + This step assumes that you already have a device registered on OpenWISP. Register or + create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *WireGuard VXLAN Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some - entries in **System Defined Variables**. It will contain internal IP address, - private and public key for the WireGuard client on the device and details of - WireGuard VPN server along with VXLAN Network Identifier(VNI) of this device. +3. Upon clicking on **Save and continue editing** button, you will see some entries in + **System Defined Variables**. It will contain internal IP address, private and public + key for the WireGuard client on the device and details of WireGuard VPN server along + with VXLAN Network Identifier(VNI) of this device. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png - :alt: WireGuard VXLAN VPN device configuration example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png + :alt: WireGuard VXLAN VPN device configuration example -**Voila!** You have successfully configured OpenWISP to manage VXLAN over -WireGuard tunnels for your devices. +**Voila!** You have successfully configured OpenWISP to manage VXLAN over WireGuard +tunnels for your devices. diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst index cd676e038..c09f92a43 100644 --- a/docs/user/wireguard.rst +++ b/docs/user/wireguard.rst @@ -1,8 +1,8 @@ Automating WireGuard Tunnels ============================ -This guide will help you to set up the automatic provisioning of -`WireGuard `_ tunnels for your devices. +This guide will help you to set up the automatic provisioning of `WireGuard +`_ tunnels for your devices. .. include:: ../partials/shared-object.rst @@ -10,103 +10,98 @@ This guide will help you to set up the automatic provisioning of ------------------------------------------------ 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. Set the **Name** of this VPN server as ``WireGuard`` and the - **Host** as ``wireguard-server.mydomain.com`` - (update this to point to your WireGuard VPN server). +2. Set the **Name** of this VPN server as ``WireGuard`` and the **Host** as + ``wireguard-server.mydomain.com`` (update this to point to your WireGuard VPN + server). 3. Select ``WireGuard`` from the dropdown as the **VPN Backend**. -4. When using WireGuard, OpenWISP takes care of managing IP addresses, - assigning an IP address to each VPN peer. Create a new subnet or select - an existing one from the dropdown menu. You can also assign an - **Internal IP** to the WireGuard Server or leave it empty for - OpenWISP to configure. This IP address will be used by the WireGuard - interface on the server. +4. When using WireGuard, OpenWISP takes care of managing IP addresses, assigning an IP + address to each VPN peer. Create a new subnet or select an existing one from the + dropdown menu. You can also assign an **Internal IP** to the WireGuard Server or + leave it empty for OpenWISP to configure. This IP address will be used by the + WireGuard interface on the server. 5. Set the **Webhook Endpoint** as - ``https://wireguard-server.mydomain.com:8081/trigger-update`` - for this example. Update this according to your VPN upgrader - endpoint. Set **Webhook AuthToken** to any strong passphrase; - this will be used to ensure that configuration upgrades are requested - from trusted sources. + ``https://wireguard-server.mydomain.com:8081/trigger-update`` for this example. + Update this according to your VPN upgrader endpoint. Set **Webhook AuthToken** to any + strong passphrase; this will be used to ensure that configuration upgrades are + requested from trusted sources. **Note**: If you are setting up a WireGuard VPN server, substitute - ``wireguard-server.mydomain.com`` with the hostname of your - VPN server and follow the steps in the next section. + ``wireguard-server.mydomain.com`` with the hostname of your VPN server and follow the + steps in the next section. -6. Under the configuration section, set the name of the WireGuard - tunnel 1 interface. In this example, we have used ``wg0``. +6. Under the configuration section, set the name of the WireGuard tunnel 1 interface. In + this example, we have used ``wg0``. .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png - :alt: WireGuard VPN server configuration example 1 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png + :alt: WireGuard VPN server configuration example 1 .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png - :alt: WireGuard VPN server configuration example 2 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png + :alt: WireGuard VPN server configuration example 2 -7. After clicking on **Save and continue editing**, you will see - that OpenWISP has automatically created public and private keys - for the WireGuard server in **System Defined Variables**, - along with internal IP address information. +7. After clicking on **Save and continue editing**, you will see that OpenWISP has + automatically created public and private keys for the WireGuard server in **System + Defined Variables**, along with internal IP address information. .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png - :alt: WireGuard VPN server configuration example 3 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png + :alt: WireGuard VPN server configuration example 3 2. Deploy WireGuard VPN Server ------------------------------ -If you haven't already set up WireGuard on your VPN server, -this would be a good time to do so. +If you haven't already set up WireGuard on your VPN server, this would be a good time to +do so. -We recommend using the -`ansible-wireguard-openwisp -`_ -role for installing WireGuard, as it also installs scripts that allow -OpenWISP to manage the WireGuard VPN server. +We recommend using the `ansible-wireguard-openwisp +`_ role for installing +WireGuard, as it also installs scripts that allow OpenWISP to manage the WireGuard VPN +server. -Ensure that the VPN server attributes used in your playbook match -the VPN server configuration in OpenWISP. +Ensure that the VPN server attributes used in your playbook match the VPN server +configuration in OpenWISP. 3. Create VPN client template for WireGuard VPN Server ------------------------------------------------------ 1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``WireGuard Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``WireGuard``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make - OpenWISP to automatically generate public and private keys and provision IP - address for each WireGuard VPN client. -6. After clicking on **Save and continue editing** button, you will see details - of *WireGuard* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. We will use the automatically generated VPN client configuration - for this example. +2. Set ``WireGuard Client`` as **Name** (you can set whatever you want) and select + ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can be + applied to. For this example, we will leave it to ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is + ``WireGuard``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP to + automatically generate public and private keys and provision IP address for each + WireGuard VPN client. +6. After clicking on **Save and continue editing** button, you will see details of + *WireGuard* VPN server in **System Defined Variables**. The template configuration + will be automatically generated which you can tweak accordingly. We will use the + automatically generated VPN client configuration for this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png - :alt: WireGuard VPN client template example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png + :alt: WireGuard VPN client template example 4. Apply WireGuard VPN template to devices ------------------------------------------ .. note:: - This step assumes that you already have a device registered on - OpenWISP. Register or create a device before proceeding. + This step assumes that you already have a device registered on OpenWISP. Register or + create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *WireGuard Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some - entries in **System Defined Variables**. It will contain internal IP address, - private and public key for the WireGuard client on the device along with - details of WireGuard VPN server. +3. Upon clicking on **Save and continue editing** button, you will see some entries in + **System Defined Variables**. It will contain internal IP address, private and public + key for the WireGuard client on the device along with details of WireGuard VPN + server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png - :alt: WireGuard VPN device configuration example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png + :alt: WireGuard VPN device configuration example -**Voila!** You have successfully configured OpenWISP to manage WireGuard -tunnels for your devices. +**Voila!** You have successfully configured OpenWISP to manage WireGuard tunnels for +your devices. diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index ca3763830..66728fa7a 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -1,8 +1,8 @@ Automating ZeroTier Tunnels =========================== -Follow the procedure described below to set up -`ZeroTier `_ tunnels on your devices. +Follow the procedure described below to set up `ZeroTier `_ +tunnels on your devices. .. include:: ../partials/shared-object.rst @@ -18,104 +18,102 @@ from the `official website `_. 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. 2. We will set **Name** of this VPN server ``ZeroTier`` and **Host** as - ``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN server). + ``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN + server). 3. Select ``ZeroTier`` from the dropdown as **VPN Backend**. -4. When using ZeroTier, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN client (ZeroTier network members)). - You can create a new subnet or select an existing one from the dropdown menu. - You can also assign an **Internal IP** to the ZeroTier controller or - leave it empty for OpenWISP to configure. This IP address will be used - to assign it to the ZeroTier controller running on the server. -5. Set the **Webhook AuthToken**, this will be the ZeroTier authorization token which you - can obtain by running the following command on the ZeroTier controller: +4. When using ZeroTier, OpenWISP takes care of managing IP addresses (assigning an IP + address to each VPN client (ZeroTier network members)). You can create a new subnet + or select an existing one from the dropdown menu. You can also assign an **Internal + IP** to the ZeroTier controller or leave it empty for OpenWISP to configure. This IP + address will be used to assign it to the ZeroTier controller running on the server. +5. Set the **Webhook AuthToken**, this will be the ZeroTier authorization token which + you can obtain by running the following command on the ZeroTier controller: .. code-block:: shell - sudo cat /var/lib/zerotier-one/authtoken.secret + sudo cat /var/lib/zerotier-one/authtoken.secret .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png - :alt: ZeroTier VPN server configuration example 1 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-1.png + :alt: ZeroTier VPN server configuration example 1 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png - :alt: ZeroTier VPN server configuration example 2 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-2.png + :alt: ZeroTier VPN server configuration example 2 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png - :alt: ZeroTier VPN server configuration example 3 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-3.png + :alt: ZeroTier VPN server configuration example 3 .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png - :alt: ZeroTier VPN server configuration example 4 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png + :alt: ZeroTier VPN server configuration example 4 -6. After clicking on **Save and continue editing**, OpenWISP automatically detects - the node address of the ZeroTier controller and creates a ZeroTier network. - The **network_id** of this network can be viewed in the **System Defined Variables** +6. After clicking on **Save and continue editing**, OpenWISP automatically detects the + node address of the ZeroTier controller and creates a ZeroTier network. The + **network_id** of this network can be viewed in the **System Defined Variables** section, where it also provides internal IP address information. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png - :alt: ZeroTier VPN server configuration example 5 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png + :alt: ZeroTier VPN server configuration example 5 3. Create VPN client template for ZeroTier VPN Server ----------------------------------------------------- 1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and - select ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can - be applied to. For this example, we will leave it to ``OpenWRT``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here - it is ``ZeroTier``. -5. Ensure that the **Automatic tunnel provisioning** option is checked. - This will enable OpenWISP to automatically provision an IP address and - ZeroTier identity secrets (used for assigning member IDs) for each ZeroTier VPN client. -6. After clicking on **Save and continue editing** button, you will see details - of *ZeroTier* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak - accordingly. - We will use the automatically generated VPN client configuration - for this example. +2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and select + ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template can be + applied to. For this example, we will leave it to ``OpenWRT``. +4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is + ``ZeroTier``. +5. Ensure that the **Automatic tunnel provisioning** option is checked. This will enable + OpenWISP to automatically provision an IP address and ZeroTier identity secrets (used + for assigning member IDs) for each ZeroTier VPN client. +6. After clicking on **Save and continue editing** button, you will see details of + *ZeroTier* VPN server in **System Defined Variables**. The template configuration + will be automatically generated which you can tweak accordingly. We will use the + automatically generated VPN client configuration for this example. .. note:: - OpenWISP uses `zerotier-idtool - `_ - to manage **ZeroTier identity secrets**. - Please make sure that you have - `ZeroTier package installed `_ - on the server. + OpenWISP uses `zerotier-idtool + `_ to + manage **ZeroTier identity secrets**. Please make sure that you have `ZeroTier + package installed `_ on the server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png - :alt: ZeroTier VPN client template example + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png + :alt: ZeroTier VPN client template example 4. Apply ZeroTier VPN template to devices ----------------------------------------- .. note:: - This step assumes that you already have a device registered on - OpenWISP. Register or create a device before proceeding. + This step assumes that you already have a device registered on OpenWISP. Register or + create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *ZeroTier Client* template. -3. Upon clicking the **Save and Continue Editing** button, you will see entries - in the **System Defined Variables** section. These entries will include **zerotier_member_id**, **identity_secret**, - and the internal **IP address** of the ZeroTier client (network member) on the device, along with details of the VPN server. +3. Upon clicking the **Save and Continue Editing** button, you will see entries in the + **System Defined Variables** section. These entries will include + **zerotier_member_id**, **identity_secret**, and the internal **IP address** of the + ZeroTier client (network member) on the device, along with details of the VPN server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png - :alt: ZeroTier VPN device configuration example 1 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png + :alt: ZeroTier VPN device configuration example 1 -4. Once the configuration is successfully applied to the device, you will notice a new ZeroTier interface - that is up and running. This interface will have the name ``owzt89f498`` (where ``owzt`` is followed - by the last six hexadecimal characters of the ZeroTier **network ID**). +4. Once the configuration is successfully applied to the device, you will notice a new + ZeroTier interface that is up and running. This interface will have the name + ``owzt89f498`` (where ``owzt`` is followed by the last six hexadecimal characters of + the ZeroTier **network ID**). .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png - :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png - :alt: ZeroTier VPN device configuration example 2 + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png + :alt: ZeroTier VPN device configuration example 2 -**Congratulations!** You've successfully configured -OpenWISP to manage ZeroTier tunnels on your devices. +**Congratulations!** You've successfully configured OpenWISP to manage ZeroTier tunnels +on your devices. From 5f417ca88384d5077da57c287c0fad80ba216b42 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Fri, 17 May 2024 20:18:54 -0400 Subject: [PATCH 17/44] [docs] Added links to all screenshots Screenshots are too big to fit in the layout, therefore we must provide a link allow readers to fully open the images in another tab/window if they want. --- docs/user/push-operations.rst | 2 ++ docs/user/rest-api.rst | 2 ++ docs/user/templates.rst | 7 +++++++ docs/user/variables.rst | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index f9d9eee50..1ae09e7c5 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -43,6 +43,7 @@ keys: -------------------------------------------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png :alt: add SSH private key as access credential in OpenWISP From the first page of OpenWISP click on "Access credentials", then click on the **"ADD @@ -62,6 +63,7 @@ system (both existing devices and devices which will be added in the future). ------------------------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png :alt: Add authorized SSH public keys template to OpenWISP (OpenWRT) Now we need to instruct your devices to allow OpenWISP accessing via SSH, in order to do diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index bb1aa6815..d6fb0377e 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -7,6 +7,7 @@ Live documentation ------------------ .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.png A general live API documentation (following the OpenAPI specification) at ``/api/v1/docs/``. @@ -17,6 +18,7 @@ Browsable web interface ----------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.png Additionally, opening any of the endpoints :ref:`listed below ` directly in the browser will show the `browsable API diff --git a/docs/user/templates.rst b/docs/user/templates.rst index dd7d3151d..295139321 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -24,6 +24,7 @@ order can be changed by drag and dropping the template in the device configurati as in the animated screenshot below. .. image:: /images/templates/template-ordering.gif + :target: ../../../../_images/template-ordering.gif :align: center :alt: Template ordering: drag and drop to change order @@ -43,6 +44,7 @@ Shared templates vs organization specific Templates can be *organization specific* or *shared* (no organization specified). .. image:: /images/templates/organization-specific-vs-shared.gif + :target: ../../../../_images/organization-specific-vs-shared.gif :align: center :alt: Shared templates vs organization specific @@ -64,6 +66,7 @@ Default Templates ----------------- .. image:: /images/templates/default-templates.gif + :target: ../../../../_images/default-templates.gif :align: center :alt: Templates enabled by default @@ -93,6 +96,7 @@ Required Templates ------------------ .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png :alt: Required template example Required templates are similar to :ref:`default_templates` but cannot be unassigned from @@ -119,6 +123,7 @@ Template tags ------------- .. image:: /images/templates/template-tags.gif + :target: ../../../../_images/template-tags.gif :align: center :alt: Template tags @@ -157,6 +162,7 @@ Once devices with the above configuration will register into the system, any tem tagged as ``mesh`` (as in the screenshot below) will be assigned to them. .. image:: /images/templates/mesh-template-tag.png + :target: ../../../../_images/mesh-template-tag.png :align: center :alt: Template tags: mesh example @@ -174,6 +180,7 @@ Once devices with the above configuration will register into the system, any tem tagged as ``dumb-ap`` (as in the screenshot below) will be assigned to them. .. image:: /images/templates/dumb-ap-template-tag.png + :target: ../../../../_images/dumb-ap-template-tag.png :align: center :alt: Template tags: dumb AP example diff --git a/docs/user/variables.rst b/docs/user/variables.rst index 496a6f9f2..6291facf8 100644 --- a/docs/user/variables.rst +++ b/docs/user/variables.rst @@ -29,6 +29,7 @@ variables" where it is possible to define the configuration variables and their as shown in the example below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png :alt: context 2 Predefined device variables @@ -58,6 +59,7 @@ You can set the *organization variables* from the organization change page **Configuration Management Settings**. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png :alt: organization variables 5. Global variables @@ -85,6 +87,7 @@ The default values of variables can be manipulated from the section "configurati variables" in the edit template page: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png :alt: default values .. _system_defined_variables: @@ -97,6 +100,7 @@ managed by the system (eg: when using templates of type VPN-client) are displaye admin UI as *System Defined Variables* in read-only mode. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png :alt: system defined variables Example usage of variables From c1bbaab78d634e44185da13c475e96e2977ce283 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 18 May 2024 12:13:04 -0400 Subject: [PATCH 18/44] [docs] Dev Utils: move signals down and link django docs Signals take a lot of space, move them to be the last section. Included /partials/signals-note.rst. --- docs/developer/utils.rst | 148 ++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 73 deletions(-) diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index e7e8fe71a..2229ebe9a 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -7,9 +7,84 @@ Code Utilities :depth: 2 :local: +.. _registering_unregistering_commands: + +Registering / Unregistering Commands +------------------------------------ + +OpenWISP Controller allows to register new command options or unregister existing +command options through two utility functions: + +- ``openwisp_controller.connection.commands.register_command`` +- ``openwisp_controller.connection.commands.unregister_command`` + +You can use these functions to register new custom commands or unregister existing +commands from your code. + +.. note:: + + These functions are to be used as an alternative to the + :ref:`OPENWISP_CONTROLLER_USER_COMMANDS` setting when :doc:`extending + openwisp-controller ` or when developing custom applications based on + OpenWISP Controller. + +``register_command`` +~~~~~~~~~~~~~~~~~~~~ + +================== ==================================================================== +Parameter Description +``command_name`` A ``str`` defining identifier for the command. +``command_config`` A ``dict`` like the one shown in :ref:`Command Configuration: schema + `. +================== ==================================================================== + +**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already +registered with the same name. + +``unregister_command`` +~~~~~~~~~~~~~~~~~~~~~~ + +================ ======================================= +Parameter Description +``command_name`` A ``str`` defining name of the command. +================ ======================================= + +**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not +exists. + +Controller Notifications +------------------------ + +The notification types registered and used by OpenWISP Controller are listed in the +following table. + +===================== ========================================================== +Notification Type Use +``config_error`` Fires when the status of a device configuration changes to + ``error``. +``device_registered`` Fires when a new device registers itself. +===================== ========================================================== + +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 + ``config_modified`` ~~~~~~~~~~~~~~~~~~~ @@ -243,76 +318,3 @@ The signal is emitted when the peers of VPN server gets changed. It is only emitted for ``Vpn`` object with **WireGuard** or **VXLAN over WireGuard** backend. - -.. _registering_unregistering_commands: - -Registering / Unregistering Commands ------------------------------------- - -OpenWISP Controller allows to register new command options or unregister existing -command options through two utility functions: - -- ``openwisp_controller.connection.commands.register_command`` -- ``openwisp_controller.connection.commands.unregister_command`` - -You can use these functions to register new custom commands or unregister existing -commands from your code. - -.. note:: - - These functions are to be used as an alternative to the - :ref:`OPENWISP_CONTROLLER_USER_COMMANDS` setting when :doc:`extending - openwisp-controller ` or when developing custom applications based on - OpenWISP Controller. - -``register_command`` -~~~~~~~~~~~~~~~~~~~~ - -================== ==================================================================== -Parameter Description -``command_name`` A ``str`` defining identifier for the command. -``command_config`` A ``dict`` like the one shown in :ref:`Command Configuration: schema - `. -================== ==================================================================== - -**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already -registered with the same name. - -``unregister_command`` -~~~~~~~~~~~~~~~~~~~~~~ - -================ ======================================= -Parameter Description -``command_name`` A ``str`` defining name of the command. -================ ======================================= - -**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not -exists. - -Controller Notifications ------------------------- - -The notification types registered and used by OpenWISP Controller are listed in the -following table. - -===================== ========================================================== -Notification Type Use -``config_error`` Fires when the status of a device configuration changes to - ``error``. -``device_registered`` Fires when a new device registers itself. -===================== ========================================================== - -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. From fe5529649585f73536c58222581548e6c64da2cc Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 18 May 2024 12:14:05 -0400 Subject: [PATCH 19/44] [docs] Capitalized case, link Django 4.2, fix broken links --- docs/developer/extending.rst | 138 +++++++++++++++++----------------- docs/user/push-operations.rst | 10 +-- docs/user/rest-api.rst | 130 ++++++++++++++++---------------- docs/user/templates.rst | 8 +- docs/user/variables.rst | 22 +++--- docs/user/vxlan-wireguard.rst | 6 +- docs/user/wireguard.rst | 6 +- docs/user/zerotier.rst | 6 +- 8 files changed, 163 insertions(+), 163 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 42ef6e59a..3943cadfc 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -1,23 +1,23 @@ -Extending openwisp-controller +Extending OpenWISP Controller ============================= .. include:: ../partials/developer-docs.rst One of the core values of the OpenWISP project is `Software Reusability `_, -for this reason *openwisp-controller* provides a set of base classes which can be +for this reason *OpenWISP Controller* 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-controller*, you need to perform +In order to implement your custom version of *OpenWISP Controller*, you need to perform the steps described in this section. When in doubt, the code in the `test project `_ will serve you as source of truth: just replicate and adapt that code to get a basic -derivative of *openwisp-controller* working. +derivative of *OpenWISP Controller* working. -If you want to add new users fields, please follow the `tutorial to extend the -openwisp-users `_. As +If you want to add new users fields, please follow the :doc:`tutorial to extend the +openwisp-users module `. As an example, we have extended *openwisp-users* to *sample_users* app and added a field ``social_security_number`` in the `sample_users/models.py `_. @@ -26,7 +26,7 @@ an example, we have extended *openwisp-users* to *sample_users* app and added a 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 project & custom apps +1. Initialize Your Project & Custom Apps ---------------------------------------- Firstly, to get started you need to create a django project: @@ -36,7 +36,7 @@ Firstly, to get started you need to create a django project: django-admin startproject mycontroller Now, you need to do is to create some new django apps which will contain your custom -version of *openwisp-controller*. +version of *OpenWISP Controller*. A django project is a collection of django apps. There are 4 django apps in the openwisp_controller project, namely config, pki, connection & geo. You'll need to create @@ -63,7 +63,7 @@ import the result into your project. For more information about how to work with django projects and django apps, please refer to the `django documentation -`_. +`_. 2. Install ``openwisp-controller`` ---------------------------------- @@ -74,8 +74,8 @@ Install (and add to the requirement of your project) openwisp-controller: pip install openwisp-controller -3. Add your apps in INSTALLED_APPS ----------------------------------- +3. Add Your Apps to ``INSTALLED_APPS`` +-------------------------------------- Now you need to add ``mycontroller.sample_config``, ``mycontroller.sample_pki``, ``mycontroller.sample_connection``, ``mycontroller.sample_geo`` & @@ -187,7 +187,7 @@ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your } ] -5. Initial Database setup +5. Initial Database Setup ------------------------- Ensure you are using one of the available geodjango backends, eg: @@ -202,7 +202,7 @@ Ensure you are using one of the available geodjango backends, eg: } For more information about GeoDjango, please refer to the `geodjango documentation -`_. +`_. 6. Django Channels Setup ------------------------ @@ -245,7 +245,7 @@ Add the following settings to ``settings.py``: } For more information about FORM_RENDERER setting, please refer to the `FORM_RENDERER -documentation `_. For +documentation `_. For more information about ASGI_APPLICATION setting, please refer to the `ASGI_APPLICATION documentation `_. @@ -253,44 +253,44 @@ For more information about CHANNEL_LAYERS setting, please refer to the `CHANNEL_ documentation `_. -6. Inherit the AppConfig class +6. Inherit the AppConfig Class ------------------------------ Please refer to the following files in the sample app of the test project: -- sample_config: +- ``sample_config``: - `sample_config/__init__.py `_. - `sample_config/apps.py `_. -- sample_geo: +- ``sample_geo``: - `sample_geo/__init__.py `_. - `sample_geo/apps.py `_. -- sample_pki: +- ``sample_pki``: - `sample_pki/__init__.py `_. - `sample_pki/apps.py `_. -- sample_connection: +- ``sample_connection``: - `sample_connection/__init__.py `_. - `sample_connection/apps.py `_. -- sample_subnet_division: +- ``sample_subnet_division``: - `sample_subnet_division/__init__.py - `_. + `_. - `sample_subnet_division/apps.py - `_. + `_. You have to replicate and adapt that code in your project. 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 ---------------------------- For the purpose of showing an example, we added a simple "details" field to the models @@ -305,15 +305,15 @@ of the sample app in the test project. - `sample_connection models `_ - `sample_subnet_division - `_ + `_ You can add fields in a similar way in your ``models.py`` file. -**Note**: for doubts regarding how to use, extend or develop models please refer to the +.. Note:: for doubts regarding how to use, extend or develop models please refer to the `"Models" section in the django documentation -`_. +`_. -8. Add swapper configurations +8. Add Swapper Configurations ----------------------------- Once you have created the models, add the following to your ``settings.py``: @@ -345,7 +345,7 @@ Once you have created the models, add the following to your ``settings.py``: Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. -9. Create database migrations +9. Create Database Migrations ----------------------------- Create database migrations: @@ -367,7 +367,7 @@ would look like: - `sample_connection/migrations/0002_default_group_permissions.py `_ - `sample_subnet_division/migrations/0002_default_group_permissions.py - `_ + `_ Create database migrations: @@ -376,9 +376,9 @@ Create database migrations: ./manage.py migrate For more information, refer to the `"Migrations" section in the django documentation -`_. +`_. -10. Create the admin +10. Create the Admin -------------------- Refer to the ``admin.py`` file of the sample app. @@ -392,24 +392,24 @@ Refer to the ``admin.py`` file of the sample app. - `sample_connection admin.py `_. - `sample_subnet_division admin.py - `_. + `_. To introduce changes to the admin, you can do it in two main ways which are described below. **Note**: for more information 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 +1. Monkey Patching ~~~~~~~~~~~~~~~~~~ If the changes you need to add are relatively small, you can resort to monkey patching. For example: -sample_config -+++++++++++++ +``sample_config`` ++++++++++++++++++ .. code-block:: python @@ -420,43 +420,43 @@ sample_config VpnAdmin, ) - # DeviceAdmin.fields += ['example'] <-- monkey patching example + DeviceAdmin.fields += ['example'] # <-- monkey patching example -sample_connection -+++++++++++++++++ +``sample_connection`` ++++++++++++++++++++++ .. code-block:: python from openwisp_controller.connection.admin import CredentialsAdmin - # CredentialsAdmin.fields += ['example'] <-- monkey patching example + CredentialsAdmin.fields += ['example'] # <-- monkey patching example -sample_geo -++++++++++ +``sample_geo`` +++++++++++++++ .. code-block:: python from openwisp_controller.geo.admin import FloorPlanAdmin, LocationAdmin - # FloorPlanAdmin.fields += ['example'] <-- monkey patching example + FloorPlanAdmin.fields += ['example'] # <-- monkey patching example -sample_pki -++++++++++ +``sample_pki`` +++++++++++++++ .. code-block:: python from openwisp_controller.pki.admin import CaAdmin, CertAdmin - # CaAdmin.fields += ['example'] <-- monkey patching example + CaAdmin.fields += ['example'] # <-- monkey patching example -sample_subnet_division -++++++++++++++++++++++ +``sample_subnet_division`` +++++++++++++++++++++++++++ .. code-block:: python from openwisp_controller.subnet_division.admin import SubnetDivisionRuleInlineAdmin - # SubnetDivisionRuleInlineAdmin.fields += ['example'] <-- monkey patching example + SubnetDivisionRuleInlineAdmin.fields += ['example'] # <-- monkey patching example 2. Inheriting admin classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -464,8 +464,8 @@ sample_subnet_division If you need to introduce significant changes and/or you don't want to resort to monkey patching, you can proceed as follows: -sample_config -+++++++++++++ +``sample_config`` ++++++++++++++++++ .. code-block:: python @@ -512,8 +512,8 @@ sample_config # add your changes here pass -sample_connection -+++++++++++++++++ +``sample_connection`` ++++++++++++++++++++++ .. code-block:: python @@ -533,8 +533,8 @@ sample_connection pass # add your changes here -sample_geo -++++++++++ +``sample_geo`` +++++++++++++++ .. code-block:: python @@ -563,8 +563,8 @@ sample_geo pass # add your changes here -sample_pki -++++++++++ +``sample_pki`` +++++++++++++++ .. code-block:: python @@ -593,8 +593,8 @@ sample_pki pass # add your changes here -sample_subnet_division -++++++++++++++++++++++ +``sample_subnet_division`` +++++++++++++++++++++++++++ .. code-block:: python @@ -632,7 +632,7 @@ sample_subnet_division pass # add your changes here -11. Create root URL configuration +11. Create Root URL Configuration --------------------------------- .. code-block:: python @@ -659,14 +659,14 @@ sample_subnet_division For more information about URL configuration in django, please refer to the `"URL dispatcher" section in the django documentation -`_. +`_. -12. Import the automated tests +12. 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-controller*. +not breaking some of the existing features of *OpenWISP Controller*. In case you need to add breaking changes, you can overwrite the tests defined in the base classes to test your own behavior. @@ -686,7 +686,7 @@ See the tests in sample_app to find out how to do this. - `sample_connection tests.py `_ - `sample_subnet_division tests.py - `_ + `_ For running the tests, you need to copy fixtures as well: @@ -704,9 +704,9 @@ You can then run tests with: Substitute ``mycontroller`` with the name you chose in step 1. For more information about automated tests in django, please refer to `"Testing in -Django" `_. +Django" `_. -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 customization. @@ -720,7 +720,7 @@ is required only when you want to make changes in the controller API, Remember t ``config_views`` location in ``urls.py`` in point 11 for extending views. For more information about django views, please refer to the `views section in the -django documentation `_. +django documentation `_. 2. Extending the Geo API Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -731,7 +731,7 @@ is required only when you want to make changes in the geo API, Remember to chang ``geo_views`` location in ``urls.py`` in point 11 for extending views. For more information about django views, please refer to the `views section in the -django documentation `_. +django documentation `_. .. _custom_subnet_division_rule_types: diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index 1ae09e7c5..7e2724d55 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -16,7 +16,7 @@ changed, OpenWISP will trigger the update in the background) and/or :doc:`firmwa upgrades (via the additional module openwisp-firmware-upgrader) <../../../openwisp-firmware-upgrader/docs/index>`. -1. Generate SSH key +1. Generate SSH Key ------------------- First of all, we need to generate the SSH key which will be used by OpenWISP to access @@ -39,8 +39,8 @@ keys: echo './sshkey' | ssh-keygen -t rsa -b 4096 -C "openwisp" -2. Save SSH private key in OpenWISP (access credentials) --------------------------------------------------------- +2. Save SSH Private Key in "Access Credentials" +----------------------------------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png @@ -59,7 +59,7 @@ Now hit save. The credentials just created will be automatically enabled for all the devices in the system (both existing devices and devices which will be added in the future). -3. Add the public key to your devices +3. Add the Public Key to Your Devices ------------------------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png @@ -83,7 +83,7 @@ Now hit save. **There's a catch**: you will need to assign the template to any existing device. -4. Test it +4. Test It ---------- Once you have performed the 3 steps above, you can test it as follows: diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index d6fb0377e..0d2d6b612 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -3,7 +3,7 @@ REST API Reference .. _controller_live_documentation: -Live documentation +Live Documentation ------------------ .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/live-docu-api.png @@ -14,7 +14,7 @@ A general live API documentation (following the OpenAPI specification) at .. _controller_browsable_web_interface: -Browsable web interface +Browsable Web Interface ----------------------- .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.png @@ -48,7 +48,7 @@ results in conjunction with the ``page`` parameter. .. _controller_rest_endpoints: -List of endpoints +List of Endpoints ----------------- Since the detailed explanation is contained in the :ref:`controller_live_documentation` @@ -56,7 +56,7 @@ and in the :ref:`controller_browsable_web_interface` of each point, here we'll p just a list of the available endpoints, for further information please open the URL of the endpoint in your browser. -List devices +List Devices ~~~~~~~~~~~~ .. code-block:: text @@ -124,21 +124,21 @@ You can filter a list of devices based on their creation time using the # Created is less than GET /api/v1/controller/device/?created__lt={creation_time} -Create device +Create Device ~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/device/ -Get device detail +Get Device Detail ~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/ -Download device configuration +Download Device Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -148,14 +148,14 @@ Download device configuration The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific device. -Change details of device +Change Details of Device ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/device/{id}/ -Patch details of device +Patch Details of Device ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -220,119 +220,119 @@ config of a device, } }' -Delete device +Delete Device ~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/device/{id}/ -List device connections +List Device Connections ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/connection/ -Create device connection +Create Device Connection ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/device/{id}/connection/ -Get device connection detail +Get Device Connection Detail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/connection/{id}/ -Change device connection detail +Change Device Connection Detail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/device/{id}/connection/{id}/ -Patch device connection detail +Patch Device Connection Detail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/device/{id}/connection/{id}/ -Delete device connection +Delete Device Connection ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/device/{id}/connection/{id}/ -List credentials +List Credentials ~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/connection/credential/ -Create credential +Create Credential ~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/connection/credential/ -Get credential detail +Get Credential Detail ~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/connection/credential/{id}/ -Change credential detail +Change Credential Detail ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/connection/credential/{id}/ -Patch credential detail +Patch Credential Detail ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/connection/credential/{id}/ -Delete credential +Delete Credential ~~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/connection/credential/{id}/ -List commands of a device +List Commands of a Device ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{id}/command/ -Execute a command a device +Execute a Command a Device ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/device/{id}/command/ -Get command details +Get Command Details ~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/device/{device_id}/command/{command_id}/ -List device groups +List Device Groups ~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -359,14 +359,14 @@ You can filter a list of device groups that have a device object using the ``emp GET /api/v1/controller/group/?empty={empty} -Create device group +Create Device Group ~~~~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/group/ -Get device group detail +Get Device Group Detail ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -375,7 +375,7 @@ Get device group detail .. _change_device_group_detail: -Change device group detail +Change Device Group Detail ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -384,7 +384,7 @@ Change device group detail This endpoint allows to change the :ref:`device_group_templates` too. -Get device group from certificate common name +Get Device Group from Certificate Common Name ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -402,7 +402,7 @@ certificate's organization as show in the example below: GET /api/v1/controller/cert/{common_name}/group/?org={org1_slug},{org2_slug} -Get device location +Get Device Location ~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -411,7 +411,7 @@ Get device location .. _create_device_location: -Create device location +Create Device Location ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -555,7 +555,7 @@ location using this endpoint. -F floorplan.floor=1 \ -F 'floorplan.image=@floorplan.png' -Change details of device location +Change Details of Device Location ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -566,14 +566,14 @@ Change details of device location objects. Refer to the :ref:`examples in the "Create device location" section ` for information on payload format. -Delete device location +Delete Device Location ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/device/{id}/location/ -Get device coordinates +Get Device Coordinates ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -590,7 +590,7 @@ as ``query_param`` because the system assumes that the device is updating it's p curl -X GET \ 'http://127.0.0.1:8000/api/v1/controller/device/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/coordinates/?key=10a0cb5a553c71099c0e4ef236435496' -Update device coordinates +Update Device Coordinates ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -625,7 +625,7 @@ as ``query_param`` because the system assumes that the device is updating it's p }, }' -List locations +List Locations ~~~~~~~~~~~~~~ .. code-block:: text @@ -645,7 +645,7 @@ that belongs to an organization. GET /api/v1/controller/location/?organization_slug={organization_slug} -Create location +Create Location ~~~~~~~~~~~~~~~ .. code-block:: text @@ -704,14 +704,14 @@ like following: "organization": "1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05" } -Get location details +Get Location Details ~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/location/{pk}/ -Change location details +Change Location Details ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -724,21 +724,21 @@ Change location details Refer to the :ref:`examples in the "Create device location" section ` for information on payload format. -Delete location +Delete Location ~~~~~~~~~~~~~~~ .. code-block:: text DELETE /api/v1/controller/location/{pk}/ -List devices in a location +List Devices in a Location ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/location/{id}/device/ -List locations with devices deployed (in GeoJSON format) +List Locations with Devices Deployed (in GeoJSON Format) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Note**: this endpoint will only list locations that have been assigned to a device. @@ -760,7 +760,7 @@ of devices from that organization. GET /api/v1/controller/location/geojson/?organization_slug={organization_slug} -List floorplans +List Floorplans ~~~~~~~~~~~~~~~ .. code-block:: text @@ -780,21 +780,21 @@ that belongs to an organization. GET /api/v1/controller/floorplan/?organization_slug={organization_slug} -Create floorplan +Create Floorplan ~~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/floorplan/ -Get floorplan details +Get Floorplan Details ~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/floorplan/{pk}/ -Change floorplan details +Change Floorplan Details ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -808,7 +808,7 @@ Delete floorplan DELETE /api/v1/controller/floorplan/{pk}/ -List templates +List Templates ~~~~~~~~~~~~~~ .. code-block:: text @@ -873,21 +873,21 @@ You can filter a list of templates based on their creation time using the GET /api/v1/controller/template/?created__lt={creation_time} -Create template +Create Template ~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/template/ -Get template detail +Get Template Detail ~~~~~~~~~~~~~~~~~~~ .. code-block:: text GET /api/v1/controller/template/{id}/ -Download template configuration +Download Template Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -897,21 +897,21 @@ Download template configuration The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific template. -Change details of template +Change Details of Template ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/template/{id}/ -Patch details of template +Patch Details of Template ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PATCH /api/v1/controller/template/{id}/ -Delete template +Delete Template ~~~~~~~~~~~~~~~ .. code-block:: text @@ -965,7 +965,7 @@ Get VPN detail GET /api/v1/controller/vpn/{id}/ -Download VPN configuration +Download VPN Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -975,14 +975,14 @@ Download VPN configuration The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific VPN. -Change details of VPN +Change Details of VPN ~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/vpn/{id}/ -Patch details of VPN +Patch Details of VPN ~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1003,14 +1003,14 @@ List CA GET /api/v1/controller/ca/ -Create new CA +Create New CA ~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/ca/ -Import existing CA +Import Existing CA ~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1027,14 +1027,14 @@ Get CA Detail GET /api/v1/controller/ca/{id}/ -Change details of CA +Change Details of CA ~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/ca/{id}/ -Patch details of CA +Patch Details of CA ~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1072,14 +1072,14 @@ List Cert GET /api/v1/controller/cert/ -Create new Cert +Create New Cert ~~~~~~~~~~~~~~~ .. code-block:: text POST /api/v1/controller/cert/ -Import existing Cert +Import Existing Cert ~~~~~~~~~~~~~~~~~~~~ .. code-block:: text @@ -1097,14 +1097,14 @@ Get Cert Detail GET /api/v1/controller/cert/{id}/ -Change details of Cert +Change Details of Cert ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text PUT /api/v1/controller/cert/{id}/ -Patch details of Cert +Patch Details of Cert ~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text diff --git a/docs/user/templates.rst b/docs/user/templates.rst index 295139321..2290c0747 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -15,7 +15,7 @@ updating the template. :backlinks: none :depth: 3 -Template ordering and override +Template Ordering and Override ------------------------------ A device can use multiple templates, **the order in which templates are assigned to each @@ -38,7 +38,7 @@ should be used as a last resort**. The recommended way to define parts of the configuration that are specific for each device is to use :doc:`Configuration variables <./variables>`. -Shared templates vs organization specific +Shared Templates vs Organization Specific ----------------------------------------- Templates can be *organization specific* or *shared* (no organization specified). @@ -119,7 +119,7 @@ for each device group. .. _templates_tags: -Template tags +Template Tags ------------- .. image:: /images/templates/template-tags.gif @@ -184,7 +184,7 @@ tagged as ``dumb-ap`` (as in the screenshot below) will be assigned to them. :align: center :alt: Template tags: dumb AP example -Implementation details of templates +Implementation Details of Templates ----------------------------------- Templates are implemented under the hood by the OpenWISP configuration engine: diff --git a/docs/user/variables.rst b/docs/user/variables.rst index 6291facf8..ed0794c77 100644 --- a/docs/user/variables.rst +++ b/docs/user/variables.rst @@ -9,7 +9,7 @@ templates, this feature is also known as *configuration context*, think of it li dictionary which is passed to the function which renders the configuration, so that it can fill variables according to the passed context. -Different types of variables +Different Types of Variables ---------------------------- The different ways in which variables are defined are described below in the order (high @@ -21,7 +21,7 @@ to low) of their precedence. .. _user_defined_variables: -1. User defined device variables +1. User Defined Device Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the device configuration section you can find a section named "Configuration @@ -32,8 +32,8 @@ as shown in the example below: :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png :alt: context -2 Predefined device variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +2. Predefined Device Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each device gets the following attributes passed as configuration variables: @@ -42,14 +42,14 @@ Each device gets the following attributes passed as configuration variables: - ``name`` - ``mac_address`` -3. Group variables +3. Group Variables ~~~~~~~~~~~~~~~~~~ Variables can also be defined in :doc:`./device-groups`. Refer to :ref:`device_group_variables` for more information. -4. Organization variables +4. Organization Variables ~~~~~~~~~~~~~~~~~~~~~~~~~ Variables can also be defined at the organization level. @@ -62,13 +62,13 @@ You can set the *organization variables* from the organization change page :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png :alt: organization variables -5. Global variables +5. Global Variables ~~~~~~~~~~~~~~~~~~~ Variables can also be defined globally using the :ref:`context_setting` setting, see also :doc:`How to Edit Django Settings <../../../../user/django-settings>`. -6. Template default values +6. Template Default Values ~~~~~~~~~~~~~~~~~~~~~~~~~~ It's possible to specify the default values of variables defined in a template. @@ -92,7 +92,7 @@ variables" in the edit template page: .. _system_defined_variables: -7. System defined variables +7. System Defined Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Predefined device variables, global variables and other variables that are automatically @@ -103,7 +103,7 @@ admin UI as *System Defined Variables* in read-only mode. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png :alt: system defined variables -Example usage of variables +Example Usage of Variables -------------------------- Here's a typical use case, the WiFi SSID and WiFi password. You don't want to define @@ -153,7 +153,7 @@ The default values can then be overridden at :ref:`device level "wlan0_password": "room_23pwd!321654" } -Implementation details of variables +Implementation Details of Variables ----------------------------------- Variables are implemented under the hood by the OpenWISP configuration engine: diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index 17a431845..e476ad1c5 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -6,7 +6,7 @@ in `WireGuard `_ tunnels which work on layer 3. .. include:: ../partials/shared-object.rst -1. Create VPN server configuration for VXLAN over WireGuard +1. Create VPN Server Configuration for VXLAN Over WireGuard ----------------------------------------------------------- 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. @@ -60,7 +60,7 @@ along with VXLAN tunnels. Pay attention to the VPN server attributes used in your playbook. It should be the same as the VPN server configuration in OpenWISP. -3. Create VPN client template for WireGuard VXLAN VPN Server +3. Create VPN Client Template for WireGuard VXLAN VPN Server ------------------------------------------------------------ 1. Visit ``/admin/config/template/add/`` to add a new template. @@ -82,7 +82,7 @@ as the VPN server configuration in OpenWISP. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png :alt: WireGuard VXLAN VPN client template example -4. Apply Wireguard VXLAN VPN template to devices +4. Apply Wireguard VXLAN VPN Template to Devices ------------------------------------------------ .. note:: diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst index c09f92a43..a2f9ca505 100644 --- a/docs/user/wireguard.rst +++ b/docs/user/wireguard.rst @@ -6,7 +6,7 @@ This guide will help you to set up the automatic provisioning of `WireGuard .. include:: ../partials/shared-object.rst -1. Create VPN server configuration for WireGuard +1. Create VPN Server Configuration for WireGuard ------------------------------------------------ 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. @@ -62,7 +62,7 @@ server. Ensure that the VPN server attributes used in your playbook match the VPN server configuration in OpenWISP. -3. Create VPN client template for WireGuard VPN Server +3. Create VPN Client Template for WireGuard VPN Server ------------------------------------------------------ 1. Visit ``/admin/config/template/add/`` to add a new template. @@ -84,7 +84,7 @@ configuration in OpenWISP. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png :alt: WireGuard VPN client template example -4. Apply WireGuard VPN template to devices +4. Apply WireGuard VPN Template to Devices ------------------------------------------ .. note:: diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index 66728fa7a..a45a9c215 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -13,7 +13,7 @@ If you haven't already set up a self-hosted ZeroTier network controller on your now is a good time to do so. You can start by simply installing ZeroTier on your server from the `official website `_. -2. Create VPN server configuration for ZeroTier +2. Create VPN Server Configuration for ZeroTier ----------------------------------------------- 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. @@ -58,7 +58,7 @@ from the `official website `_. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png :alt: ZeroTier VPN server configuration example 5 -3. Create VPN client template for ZeroTier VPN Server +3. Create VPN Client Template for ZeroTier VPN Server ----------------------------------------------------- 1. Visit ``/admin/config/template/add/`` to add a new template. @@ -87,7 +87,7 @@ from the `official website `_. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png :alt: ZeroTier VPN client template example -4. Apply ZeroTier VPN template to devices +4. Apply ZeroTier VPN Template to Devices ----------------------------------------- .. note:: From c00fdb7926c054c3814a15f33305e264ca966136 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 18 May 2024 12:14:18 -0400 Subject: [PATCH 20/44] [docs] Updated UI instructions of push operations --- docs/user/push-operations.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index 7e2724d55..065280e3b 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -46,9 +46,10 @@ keys: :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png :alt: add SSH private key as access credential in OpenWISP -From the first page of OpenWISP click on "Access credentials", then click on the **"ADD -ACCESS CREDENTIALS"** button in the upper right corner (alternatively, go to the -following URL: ``/admin/connection/credentials/add/``). +From the first page of OpenWISP click on "CONFIGURATIONS" in the left navigation menu, +then "Access credentials", then click on the **"ADD ACCESS CREDENTIALS"** button in the +upper right corner (alternatively, go to the following URL path: +``/admin/connection/credentials/add/``). Select SSH as ``type``, enable the **Auto add** checkbox, then at the field "Credentials type" select "SSH (private key)", now type "root" in the ``username`` field, while in @@ -70,9 +71,9 @@ Now we need to instruct your devices to allow OpenWISP accessing via SSH, in ord this we need to add the contents of the public key file created in step 1 (``sshkey.pub``) in the file ``/etc/dropbear/authorized_keys`` on the devices, the recommended way to do this is to create a configuration template in OpenWISP: from the -first page of OpenWISP, click on "Templates", then and click on the **"ADD TEMPLATE"** -button in the upper right corner (alternatively, go to the following URL: -``/admin/config/template/add/``). +first page of OpenWISP, click on "CONFIGURATIONS" in the left navigation menu, then and +click on the **"ADD TEMPLATE"** button in the upper right corner (alternatively, go to +the following URL: ``/admin/config/template/add/``). Check **enabled by default**, then scroll down the configuration section, click on "Configuration Menu", scroll down, click on "Files" then close the menu by clicking From 7675d56e85f3215fb8bb0442defa9e9bfce997b0 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 18 May 2024 14:45:17 -0400 Subject: [PATCH 21/44] [docs] Fixed a few more issues in extending.rst --- docs/developer/extending.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 3943cadfc..2960ee547 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -17,8 +17,8 @@ serve you as source of truth: just replicate and adapt that code to get a basic derivative of *OpenWISP Controller* working. If you want to add new users fields, please follow the :doc:`tutorial to extend the -openwisp-users module `. As -an example, we have extended *openwisp-users* to *sample_users* app and added a field +openwisp-users module `. As an +example, we have extended *openwisp-users* to *sample_users* app and added a field ``social_security_number`` in the `sample_users/models.py `_. @@ -309,9 +309,11 @@ of the sample app in the test project. You can add fields in a similar way in your ``models.py`` file. -.. Note:: for doubts regarding how to use, extend or develop models please refer to the -`"Models" section in the django documentation -`_. +.. note:: + + If you have any doubt regarding how to use, extend or develop models please refer to + the `"Models" section in the django documentation + `_. 8. Add Swapper Configurations ----------------------------- @@ -420,7 +422,7 @@ For example: VpnAdmin, ) - DeviceAdmin.fields += ['example'] # <-- monkey patching example + DeviceAdmin.fields += ["example"] # <-- monkey patching example ``sample_connection`` +++++++++++++++++++++ @@ -429,7 +431,7 @@ For example: from openwisp_controller.connection.admin import CredentialsAdmin - CredentialsAdmin.fields += ['example'] # <-- monkey patching example + CredentialsAdmin.fields += ["example"] # <-- monkey patching example ``sample_geo`` ++++++++++++++ @@ -438,7 +440,7 @@ For example: from openwisp_controller.geo.admin import FloorPlanAdmin, LocationAdmin - FloorPlanAdmin.fields += ['example'] # <-- monkey patching example + FloorPlanAdmin.fields += ["example"] # <-- monkey patching example ``sample_pki`` ++++++++++++++ @@ -447,7 +449,7 @@ For example: from openwisp_controller.pki.admin import CaAdmin, CertAdmin - CaAdmin.fields += ['example'] # <-- monkey patching example + CaAdmin.fields += ["example"] # <-- monkey patching example ``sample_subnet_division`` ++++++++++++++++++++++++++ @@ -456,7 +458,7 @@ For example: from openwisp_controller.subnet_division.admin import SubnetDivisionRuleInlineAdmin - SubnetDivisionRuleInlineAdmin.fields += ['example'] # <-- monkey patching example + SubnetDivisionRuleInlineAdmin.fields += ["example"] # <-- monkey patching example 2. Inheriting admin classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From e25786d0b9ef9a310d87e2cd3352b7eb74cb17f9 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Fri, 24 May 2024 19:23:12 -0400 Subject: [PATCH 22/44] [chores] Updated internal references after strucutral change --- docs/developer/extending.rst | 2 +- docs/developer/utils.rst | 4 ++-- docs/partials/developer-docs.rst | 2 +- docs/user/intro.rst | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 2960ee547..fd8941ada 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -17,7 +17,7 @@ serve you as source of truth: just replicate and adapt that code to get a basic derivative of *OpenWISP Controller* working. If you want to add new users fields, please follow the :doc:`tutorial to extend the -openwisp-users module `. As an +openwisp-users module `. As an example, we have extended *openwisp-users* to *sample_users* app and added a field ``social_security_number`` in the `sample_users/models.py `_. diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 2229ebe9a..8aabfa2d8 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -73,11 +73,11 @@ 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 +` to send notifications for this type. Signals diff --git a/docs/partials/developer-docs.rst b/docs/partials/developer-docs.rst index 4e82c555a..f025d86e9 100644 --- a/docs/partials/developer-docs.rst +++ b/docs/partials/developer-docs.rst @@ -9,4 +9,4 @@ OpenWISP, please refer to: - :doc:`General OpenWISP Quickstart ` - - :doc:`OpenWISP Controller User Docs ` + - :doc:`OpenWISP Controller User Docs ` diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 5ede6f1a7..f4aaa7451 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -65,7 +65,7 @@ order to perform :doc:`push operations `, e.g.: - Sending configuration updates. - :doc:`Executing shell commands `. - Perform firmware upgrades via the additional :doc:`firmware upgrade module - `. + `. The default connection protocol implemented is SSH, but other protocol mechanism is extensible and custom protocols can be implemented as well. @@ -79,7 +79,7 @@ SNMP ~~~~ The SNMP connector is useful to collect monitoring information and it's used in -:doc:`OpenWISP Monitoring ` for performing +:doc:`OpenWISP Monitoring ` for performing checks to collect monitoring information. `Read more `_ on how to use it. From bab88f120eb635124ef2b4ed3037698094806fec Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 29 May 2024 15:22:23 +0530 Subject: [PATCH 23/44] [docs] Added label for OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY --- docs/user/settings.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 58f88b292..95f22be29 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -572,6 +572,8 @@ field reset to empty state to avoid potential conflicts. Set this to ``False`` if every organization has its dedicated management tunnel with a dedicated address space that is reachable by the OpenWISP server. +.. _openwisp_controller_management_ip_only: + ``OPENWISP_CONTROLLER_MANAGEMENT_IP_ONLY`` ------------------------------------------ From 29289d8ba0122f397707a7d64afe59eb131a5c13 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 29 May 2024 16:06:31 -0400 Subject: [PATCH 24/44] [docs] Updated install instructions --- docs/developer/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index e40350e12..914009c8f 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -19,7 +19,7 @@ Install the system dependencies: 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 chromium + sudo apt install -y chromium-browser Fork and clone the forked repository: @@ -59,7 +59,7 @@ Install development dependencies: pip install -e . pip install -r requirements-test.txt - npm install -g jshint stylelint + sudo npm install -g jshint stylelint Install WebDriver for Chromium for your browser version from https://chromedriver.chromium.org/home and Extract ``chromedriver`` to one of From 79fe1a412b06067905dc3eb1df31148f5d367f03 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 29 May 2024 16:06:44 -0400 Subject: [PATCH 25/44] [docs] Updated references to other modules --- docs/user/push-operations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index 065280e3b..907ae1964 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -14,7 +14,7 @@ Follow the procedure described below to enable secure SSH access from OpenWISP t devices, this is required to enable push operations (whenever the configuration is changed, OpenWISP will trigger the update in the background) and/or :doc:`firmware upgrades (via the additional module openwisp-firmware-upgrader) -<../../../openwisp-firmware-upgrader/docs/index>`. +`. 1. Generate SSH Key ------------------- From 100bbc31395b1bf494e55e2ef532b2a314997170 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Fri, 31 May 2024 18:16:42 -0400 Subject: [PATCH 26/44] [docs] Removed :orphan: directives from README and CONTRIBUTING We solved this problem in a different way. Github doesn't render these sphinx directives properly. --- CHANGES.rst | 2 -- CONTRIBUTING.rst | 2 -- README.rst | 2 -- 3 files changed, 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 305c1bd47..899d22b6d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,3 @@ -:orphan: - Changelog ========= diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d7868e7e3..79c907b8c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,3 +1 @@ -:orphan: - Please refer to the `Contribution Guidelines `_. diff --git a/README.rst b/README.rst index 21ec3a43a..96a23c503 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -:orphan: - openwisp-controller =================== From 52fe337778b0ab9ffdd836283c3a65479978c274 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 5 Jun 2024 15:53:18 -0400 Subject: [PATCH 27/44] [docs] Capitalized headings in installation.rst --- docs/developer/installation.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 914009c8f..e60dffe59 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -9,7 +9,7 @@ Dependencies - Python >= 3.8 - OpenSSL -Installing for development +Installing for Development -------------------------- Install the system dependencies: @@ -101,7 +101,7 @@ Run quality assurance tests with: ./run-qa-checks -Alternative sources +Alternative Sources ------------------- Pypi @@ -128,7 +128,7 @@ Alternatively you can use the git protocol: pip install -e git+git://github.com/openwisp/openwisp-controller#egg=openwisp_controller -Install and run on Docker +Install and Run on Docker ------------------------- .. warning:: @@ -150,12 +150,12 @@ Run the docker container: docker-compose up -Troubleshooting steps for common installation issues +Troubleshooting Steps for Common Installation Issues ---------------------------------------------------- You may encounter some issues while installing GeoDjango. -Unable to load SpatiaLite library extension? +Unable to Load SpatiaLite library Extension? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are incurring in the following exception: @@ -168,7 +168,7 @@ You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as expla `django documentation regarding how to install and configure spatialte `_. -Having Issues with other geospatial libraries? +Having Issues with Other Geospatial Libraries? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Please refer `troubleshooting issues related to geospatial libraries From 2e5cb82053f57e31babccbd09e88fcd4eec858dc Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 6 Jun 2024 17:29:07 -0400 Subject: [PATCH 28/44] [docs] Clarified how to run selenium tests --- docs/developer/installation.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index e60dffe59..2b75f023f 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -92,9 +92,21 @@ Run tests with: .. code-block:: shell ./runtests.py --parallel - # To run database tests against PostgreSQL backend + +Some tests, such as the Selenium UI tests, require a PostgreSQL database to run. If you +don't have a PostgreSQL database running on your system, you can use :ref:`the Docker +Compose configuration provided in this repository `. Once set up, +you can run these specific tests as follows: + +.. code-block:: shell + + # Run database tests against PostgreSQL backend POSTGRESQL=1 ./runtests.py --parallel + # Run only specific selenium tests classes + cd tests/ + DJANGO_SETTINGS_MODULE=openwisp2.postgresql_settings ./manage.py test openwisp_controller.config.tests.test_selenium.TestDeviceAdmin + Run quality assurance tests with: .. code-block:: shell @@ -128,6 +140,8 @@ Alternatively you can use the git protocol: pip install -e git+git://github.com/openwisp/openwisp-controller#egg=openwisp_controller +.. _controller_dev_docker: + Install and Run on Docker ------------------------- From 633e672b8c0d97d554d3edd2426686ed52bffe7a Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 7 Jun 2024 22:32:03 +0530 Subject: [PATCH 29/44] [skip ci][docs] Added reference labels --- docs/user/settings.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 95f22be29..5eff43ef5 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -206,6 +206,8 @@ automatic registration `_ for more information. +.. _openwisp_controller_consistent_registration: + ``OPENWISP_CONTROLLER_CONSISTENT_REGISTRATION`` ----------------------------------------------- @@ -364,6 +366,8 @@ enforce this is case-insensitive. Note: For this constraint to be optional, it is enforced on an application level and not on database. +.. _openwisp_controller_hardware_id_enabled: + ``OPENWISP_CONTROLLER_HARDWARE_ID_ENABLED`` ------------------------------------------- From 6f1e89bd995525c58a463f47e349db3d8699d98a Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 19 Jun 2024 19:17:09 +0530 Subject: [PATCH 30/44] [skip ci] Fixed references --- docs/developer/utils.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 8aabfa2d8..b618d5183 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -73,11 +73,11 @@ 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 +` to send notifications for this type. Signals From f4d30786f26d049a37635af4715471cda473de49 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Fri, 5 Jul 2024 18:19:04 -0400 Subject: [PATCH 31/44] [docs] Minor improvements --- docs/developer/extending.rst | 6 +++--- docs/developer/utils.rst | 10 ++++------ docs/user/intro.rst | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index fd8941ada..0c56199b2 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -17,9 +17,9 @@ serve you as source of truth: just replicate and adapt that code to get a basic derivative of *OpenWISP Controller* working. If you want to add new users fields, please follow the :doc:`tutorial to extend the -openwisp-users module `. As an -example, we have extended *openwisp-users* to *sample_users* app and added a field -``social_security_number`` in the `sample_users/models.py +openwisp-users module `. As an example, we have extended +*openwisp-users* to *sample_users* app and added a field ``social_security_number`` in +the `sample_users/models.py `_. **Premise**: if you plan on using a customized version of this module, we suggest to diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index b618d5183..3acb29c2a 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -72,13 +72,11 @@ You can define your own notification types using ``register_notification_type`` from OpenWISP Notifications. For more information, see the relevant :doc:`documentation section about registering -notification types in the Notifications module -`. +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. +Once a new notification type is registered, you can use the :doc:`"notify" signal +provided by the Notifications module ` to +send notifications with this new type. Signals ------- diff --git a/docs/user/intro.rst b/docs/user/intro.rst index f4aaa7451..cf7b56905 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -79,8 +79,8 @@ SNMP ~~~~ The SNMP connector is useful to collect monitoring information and it's used in -:doc:`OpenWISP Monitoring ` for performing -checks to collect monitoring information. `Read more +:doc:`OpenWISP Monitoring ` for performing checks to collect +monitoring information. `Read more `_ on how to use it. From f1b0a21d2a80d39e7474c6949083434ab65864e2 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 8 Jul 2024 18:46:37 +0530 Subject: [PATCH 32/44] [skip ci] Fixed target of images --- docs/user/templates.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/user/templates.rst b/docs/user/templates.rst index 2290c0747..6ab656514 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -24,7 +24,7 @@ order can be changed by drag and dropping the template in the device configurati as in the animated screenshot below. .. image:: /images/templates/template-ordering.gif - :target: ../../../../_images/template-ordering.gif + :target: ../../_images/template-ordering.gif :align: center :alt: Template ordering: drag and drop to change order @@ -44,7 +44,7 @@ Shared Templates vs Organization Specific Templates can be *organization specific* or *shared* (no organization specified). .. image:: /images/templates/organization-specific-vs-shared.gif - :target: ../../../../_images/organization-specific-vs-shared.gif + :target: ../../_images/organization-specific-vs-shared.gif :align: center :alt: Shared templates vs organization specific @@ -66,7 +66,7 @@ Default Templates ----------------- .. image:: /images/templates/default-templates.gif - :target: ../../../../_images/default-templates.gif + :target: ../../_images/default-templates.gif :align: center :alt: Templates enabled by default @@ -123,7 +123,7 @@ Template Tags ------------- .. image:: /images/templates/template-tags.gif - :target: ../../../../_images/template-tags.gif + :target: ../../_images/template-tags.gif :align: center :alt: Template tags @@ -162,7 +162,7 @@ Once devices with the above configuration will register into the system, any tem tagged as ``mesh`` (as in the screenshot below) will be assigned to them. .. image:: /images/templates/mesh-template-tag.png - :target: ../../../../_images/mesh-template-tag.png + :target: ../../_images/mesh-template-tag.png :align: center :alt: Template tags: mesh example @@ -180,7 +180,7 @@ Once devices with the above configuration will register into the system, any tem tagged as ``dumb-ap`` (as in the screenshot below) will be assigned to them. .. image:: /images/templates/dumb-ap-template-tag.png - :target: ../../../../_images/dumb-ap-template-tag.png + :target: ../../_images/dumb-ap-template-tag.png :align: center :alt: Template tags: dumb AP example From c302cff00acabbbc168e6a60c8f6a12f1ee0d39b Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 11 Jul 2024 17:42:05 -0400 Subject: [PATCH 33/44] [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 7c0823d3b..7b1d319f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,9 @@ Controller ========== +**Source code**: `github.com/openwisp/openwisp-controller +`_. + OpenWISP Controller is responsible of of managing the core resources of the network and allows automating several aspects like adoption, provisioning, VPN tunnel configuration, generation of X509 certificates, subnet and IP address allocation and more. From c9309fe7d8cb1d937c9a527d1c8ea63193b80038 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 15 Jul 2024 16:44:23 +0530 Subject: [PATCH 34/44] [skip ci] Fixed references to OpenWISP modules --- README.rst | 14 +++++++------- docs/developer/installation.rst | 4 ++-- docs/user/settings.rst | 12 ++++++------ docs/user/templates.rst | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 96a23c503..070319016 100644 --- a/README.rst +++ b/README.rst @@ -48,28 +48,28 @@ on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: -- `openwisp-monitoring `_: +- `openwisp-monitoring `_: provides device status monitoring, collection of metrics, charts, alerts, possibility to define custom checks -- `openwisp-firmware-upgrader `_: +- `openwisp-firmware-upgrader `_: automated firmware upgrades (single devices or mass network upgrades) -- `openwisp-radius `_: +- `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-network-topology `_: +- `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-ipam `_: +- `openwisp-ipam `_: allows to manage the assignment of IP addresses used in the network -- `openwisp-notifications `_: +- `openwisp-notifications `_: allows users to be aware of important events happening in the network. **For a more complete overview of the OpenWISP modules and architecture**, see the `OpenWISP Architecture Overview -`_. +`_. .. image:: https://raw.githubusercontent.com/openwisp/openwisp2-docs/master/assets/design/openwisp-logo-black.svg :target: http://openwisp.org diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 2b75f023f..23cfb5abe 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -149,8 +149,8 @@ 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-openwisp + `. Build from the Dockerfile: diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 5eff43ef5..8c1c7e2fe 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -201,9 +201,9 @@ Whether devices can automatically register through the controller or not. This feature is enabled by default. -Autoregistration must be supported on the devices in order to work, see `openwisp-config -automatic registration -`_ for more +Auto-registration must be supported on the devices in order to work, see +:doc:`openwisp-config automatic registration +` for more information. .. _openwisp_controller_consistent_registration: @@ -221,9 +221,9 @@ hence keeping the existing configuration without creating a new one. This feature is enabled by default. -Autoregistration must be enabled also on the devices in order to work, see -`openwisp-config consistent key generation -`_ for more +Auto-registration must be enabled also on the devices in order to work, see +:ref:`openwisp-config consistent key generation +` for more information. ``OPENWISP_CONTROLLER_REGISTRATION_SELF_CREATION`` diff --git a/docs/user/templates.rst b/docs/user/templates.rst index 6ab656514..cbd60a04f 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -144,8 +144,8 @@ in specific templates which are then tagged with specific keywords: - ``mesh``: tag to use for mesh configuration - ``dumb-ap``: tag to use for dumb AP configuration -The `openwisp-config -`_ configuration of +The :ref:`openwisp-config +` configuration of each device type must specify the correct tag before each device registers in the system. From 684c6144f6ec5dbc82bb2bb8052e7b8535e15930 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 15 Jul 2024 19:27:06 +0530 Subject: [PATCH 35/44] [skip ci] Updated docs website URL --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 070319016..4d819cb6e 100644 --- a/README.rst +++ b/README.rst @@ -48,28 +48,28 @@ on top of its building blocks. Other popular building blocks that are part of the OpenWISP ecosystem are: -- `openwisp-monitoring `_: +- `openwisp-monitoring `_: provides device status monitoring, collection of metrics, charts, alerts, possibility to define custom checks -- `openwisp-firmware-upgrader `_: +- `openwisp-firmware-upgrader `_: automated firmware upgrades (single devices or mass network upgrades) -- `openwisp-radius `_: +- `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-network-topology `_: +- `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-ipam `_: +- `openwisp-ipam `_: allows to manage the assignment of IP addresses used in the network -- `openwisp-notifications `_: +- `openwisp-notifications `_: allows users to be aware of important events happening in the network. **For a more complete overview of the OpenWISP modules and architecture**, see the `OpenWISP Architecture Overview -`_. +`_. .. image:: https://raw.githubusercontent.com/openwisp/openwisp2-docs/master/assets/design/openwisp-logo-black.svg :target: http://openwisp.org From a3bfc5fa6977c9438ba843e5d3d27d92689f6ecd Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 15 Jul 2024 20:36:05 -0400 Subject: [PATCH 36/44] [docs] Table of contents, consistency, fixes [skip ci] --- docs/developer/extending.rst | 15 ++++++++++----- docs/developer/installation.rst | 6 +++++- docs/developer/utils.rst | 2 +- docs/user/device-groups.rst | 4 +--- docs/user/intro.rst | 4 ++-- docs/user/push-operations.rst | 9 ++++++++- docs/user/rest-api.rst | 4 ++++ docs/user/shell-commands.rst | 7 +++++++ docs/user/subnet-division-rules.rst | 4 ++++ docs/user/templates.rst | 13 +++++++++---- docs/user/vxlan-wireguard.rst | 6 +++++- docs/user/wireguard.rst | 6 +++++- docs/user/zerotier.rst | 6 +++++- 13 files changed, 66 insertions(+), 20 deletions(-) diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 0c56199b2..051d081a3 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -3,8 +3,7 @@ Extending OpenWISP Controller .. 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 Controller* provides a set of base classes which can be imported, extended and reused to create derivative apps. @@ -22,9 +21,15 @@ openwisp-users module `. As an example, we have exte the `sample_users/models.py `_. -**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 Project & Custom Apps ---------------------------------------- diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 23cfb5abe..2761e9b77 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 ------------ @@ -85,7 +89,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: diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 3acb29c2a..4d0a0d881 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -3,7 +3,7 @@ Code Utilities .. include:: ../partials/developer-docs.rst -.. contents:: +.. contents:: **Table of Contents**: :depth: 2 :local: diff --git a/docs/user/device-groups.rst b/docs/user/device-groups.rst index c9ca03d18..40f00578e 100644 --- a/docs/user/device-groups.rst +++ b/docs/user/device-groups.rst @@ -6,9 +6,7 @@ a common characteristic but also some kind of organizational need: they need to specific configuration templates, variables and/or associated metadata which differs from the rest of the network. -Features provided by Device Groups: - -.. contents:: +.. contents:: **Features provided by Device Groups:** :depth: 2 :local: diff --git a/docs/user/intro.rst b/docs/user/intro.rst index cf7b56905..361758226 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -3,7 +3,7 @@ Controller: Structure & Features OpenWISP Controller is a Python package which ships five Django apps. -.. contents:: +.. contents:: **These Django apps are listed below**: :depth: 1 :local: @@ -14,7 +14,7 @@ The config app is the core of the controller module and implements all the follo features: - **Configuration management** for embedded devices supporting: - - `OpenWRT `_ + - `OpenWrt `_ - `OpenWISP Firmware `_ - additional firmware can be added by :ref:`specifying custom configuration backends ` diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index 907ae1964..0aa927418 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -1,6 +1,13 @@ Configuring Push Operations =========================== +.. contents:: **Table of Contents**: + :depth: 2 + :local: + +Introduction +------------ + .. important:: If you have installed OpenWISP with the `ansbile-openwisp2 role @@ -65,7 +72,7 @@ system (both existing devices and devices which will be added in the future). .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png - :alt: Add authorized SSH public keys template to OpenWISP (OpenWRT) + :alt: Add authorized SSH public keys template to OpenWISP (OpenWrt) Now we need to instruct your devices to allow OpenWISP accessing via SSH, in order to do this we need to add the contents of the public key file created in step 1 diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 0d2d6b612..3b7041c9e 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: + .. _controller_live_documentation: Live Documentation diff --git a/docs/user/shell-commands.rst b/docs/user/shell-commands.rst index d64e3accc..626fd064b 100644 --- a/docs/user/shell-commands.rst +++ b/docs/user/shell-commands.rst @@ -1,6 +1,13 @@ Sending Commands to Devices =========================== +.. contents:: **Table of Contents**: + :depth: 3 + :local: + +Default Commands +---------------- + By default, there are three options in the **Send Command** dropdown: 1. Reboot diff --git a/docs/user/subnet-division-rules.rst b/docs/user/subnet-division-rules.rst index bad831009..6e56d9692 100644 --- a/docs/user/subnet-division-rules.rst +++ b/docs/user/subnet-division-rules.rst @@ -4,6 +4,10 @@ Automating Subnet and IP Address Provisioning This guide helps you automate provisioning subnets and IP addresses for your network devices. +.. contents:: **Table of Contents**: + :depth: 2 + :local: + .. _step1_rule: 1. Create a Subnet and a Subnet Division Rule diff --git a/docs/user/templates.rst b/docs/user/templates.rst index cbd60a04f..564fc9c06 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -1,6 +1,13 @@ Configuration Templates ======================= +.. contents:: **Table of Contents**: + :depth: 3 + :local: + +What is a Template? +------------------- + Templates are designed to store configuration that can be reused by some or all the devices in the system. @@ -11,10 +18,6 @@ This means that configuration can be defined only once for multiple devices, and need to update a specific piece of configuration arises, it can be easily achieved by updating the template. -.. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 - Template Ordering and Override ------------------------------ @@ -38,6 +41,8 @@ should be used as a last resort**. The recommended way to define parts of the configuration that are specific for each device is to use :doc:`Configuration variables <./variables>`. +.. _controller_shared_vs_org: + Shared Templates vs Organization Specific ----------------------------------------- diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index e476ad1c5..aa563f027 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -6,6 +6,10 @@ in `WireGuard `_ tunnels which work on layer 3. .. include:: ../partials/shared-object.rst +.. contents:: **Table of Contents**: + :depth: 2 + :local: + 1. Create VPN Server Configuration for VXLAN Over WireGuard ----------------------------------------------------------- @@ -67,7 +71,7 @@ as the VPN server configuration in OpenWISP. 2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and select ``VPN-client`` as **type** from the dropdown list. 3. The **Backend** field refers to the backend of the device this template can be - applied to. For this example, we will leave it as ``OpenWRT``. + applied to. For this example, we will leave it as ``OpenWrt``. 4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is ``Wireguard VXLAN``. 5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst index a2f9ca505..c11efbbde 100644 --- a/docs/user/wireguard.rst +++ b/docs/user/wireguard.rst @@ -6,6 +6,10 @@ This guide will help you to set up the automatic provisioning of `WireGuard .. include:: ../partials/shared-object.rst +.. contents:: **Table of Contents**: + :depth: 2 + :local: + 1. Create VPN Server Configuration for WireGuard ------------------------------------------------ @@ -69,7 +73,7 @@ configuration in OpenWISP. 2. Set ``WireGuard Client`` as **Name** (you can set whatever you want) and select ``VPN-client`` as **type** from the dropdown list. 3. The **Backend** field refers to the backend of the device this template can be - applied to. For this example, we will leave it to ``OpenWRT``. + applied to. For this example, we will leave it to ``OpenWrt``. 4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is ``WireGuard``. 5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP to diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index a45a9c215..e61a52be2 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -6,6 +6,10 @@ tunnels on your devices. .. include:: ../partials/shared-object.rst +.. contents:: **Table of Contents**: + :depth: 2 + :local: + 1. Configure Self-Hosted ZeroTier Network Controller ---------------------------------------------------- @@ -65,7 +69,7 @@ from the `official website `_. 2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and select ``VPN-client`` as **type** from the dropdown list. 3. The **Backend** field refers to the backend of the device this template can be - applied to. For this example, we will leave it to ``OpenWRT``. + applied to. For this example, we will leave it to ``OpenWrt``. 4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is ``ZeroTier``. 5. Ensure that the **Automatic tunnel provisioning** option is checked. This will enable From 3dbb1ac3cdf42065ae04a6bdd1568eb80589ae76 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 18:53:30 -0400 Subject: [PATCH 37/44] [docs] Added OpenVPN page --- docs/index.rst | 1 + docs/user/openvpn.rst | 223 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 docs/user/openvpn.rst diff --git a/docs/index.rst b/docs/index.rst index 7b1d319f8..148869423 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ For a full introduction please refer to :doc:`user/intro`. user/wireguard.rst user/vxlan-wireguard.rst user/zerotier.rst + user/openvpn.rst user/subnet-division-rules.rst user/rest-api.rst user/settings.rst diff --git a/docs/user/openvpn.rst b/docs/user/openvpn.rst new file mode 100644 index 000000000..245a307d7 --- /dev/null +++ b/docs/user/openvpn.rst @@ -0,0 +1,223 @@ +Automating OpenVPN Tunnels +========================== + +In this guide, we will explore how to set up the automatic provisioning +and management of **OpenVPN tunnels**. + +.. contents:: **Table of Contents**: + :backlinks: none + :depth: 3 + +Setting up the OpenVPN Server +----------------------------- + +The first step is to install the OpenVPN server. In this tutorial, to +perform this step we will use Ansible. + +If you already have experience installing an OpenVPN server, feel free to +use any method you prefer. + +.. important:: + + If you have already set up your OpenVPN server or prefer to install + the OpenVPN server using a different method, you can skip forward to + :ref:`import_ca_and_server_cert`. + +For simplicity, **the OpenVPN server must be installed on the same server +where OpenWISP is also installed**. + +While it is possible to install the OpenVPN server on a different server, +it requires additional steps not covered in this tutorial. + +1. Install Ansible and Required Ansible Roles +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install Ansible **on your local machine** (please ensure that you do not +install it on the server). + +To **install Ansible**, we suggest following the official `Ansible +installation guide +`_. + +After installing Ansible, you need to install Git (example for Linux +Debian/Ubuntu systems): + +.. code-block:: bash + + sudo apt-get install git + +After installing both Ansible and Git, install the required roles: + +.. code-block:: bash + + ansible-galaxy install git+https://github.com/Stouts/Stouts.openvpn,3.0.0 nkakouros.easyrsa + +2. Create Inventory File and Playbook YAML +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create an Ansible inventory file named ``inventory`` **on your local +machine** (not on the server) with the following contents: + +.. code-block:: + + [openvpn] + your_server_domain_or_ip + +For example, if your server IP is ``192.168.56.2``: + +.. code-block:: + + [openvpn] + 192.168.56.2 + +In the same directory where you created the ``inventory`` file, create a +file named ``playbook.yml`` with the following content: + +.. code-block:: yaml + + - hosts: openvpn + vars: + # EasyRSA + easyrsa_generate_dh: true + easyrsa_servers: + - name: server + easyrsa_clients: [] + easyrsa_pki_dir: /etc/easyrsa/pki + + # OpenVPN + openvpn_keydir: "{{ easyrsa_pki_dir }}" + openvpn_clients: [] + openvpn_use_pam: false + roles: + - role: nkakouros.easyrsa + - role: Stouts.openvpn + +.. hint:: + + You can further customize the configuration using the role variables. + Read more about other options in `EasyRSA + `_ and + `OpenVPN `_. + +3. Run the Playbook +~~~~~~~~~~~~~~~~~~~ + +Run the Ansible playbook: + +.. code-block:: bash + + ansible-playbook -i inventory playbook.yml -b -k -K --become-method=su + +.. _import_ca_and_server_cert: + +Import the CA and the Server Certificate in OpenWISP +---------------------------------------------------- + +.. important:: + + If you chose an alternative installation method for OpenVPN and you + did not create the CA and certificate yet, you can create the + certificates from scratch via the OpenWISP web interface instead of + importing them. + + Follow the instructions below and instead of selecting + :guilabel:`Import Existing` as :guilabel:`Operation Type`, select + :guilabel:`Create new`. + + You also won't need to copy any file from the server as OpenWISP + generates the x509 certificates automatically. + +To import the CA and Server Certificate into OpenWISP, you need to access +your server via ``ssh`` or any other method that suits you. + +Change your directory to ``/etc/easyrsa/pki/``. + +.. note:: + + If you incurr inthe following error: ``-bash: cd: /etc/easyrsa/pki: + Permission denied``, you may need to log in as the root user. + +Import the CA +~~~~~~~~~~~~~ + +In your OpenWISP dashboard, go to ``/admin/pki/ca/add/``. + +In :guilabel:`Operation Type`, choose :guilabel:`Import Existing`. + +Get your CA certificate from the ``ca.crt`` file and the private key from +the ``private/ca.key`` file, then enter them in the respective fields. + +Import the Server Certificate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In your OpenWISP dashboard, go to ``/admin/pki/cert/add/``. + +In :guilabel:`Operation Type`, choose :guilabel:`Import Existing` and in +**CA**, choose the CA you just created. + +Get your server certificate from the ``issued/server.crt`` file and the +server private key from the ``private/server.key`` file, then enter them +in the respective fields. + +Create the VPN Server in OpenWISP +--------------------------------- + +In the OpenWISP dashboard, go to ``/admin/config/vpn/add/``. + +In the :guilabel:`Host` field, enter your server IP address. In the +:guilabel:`Certification Authority` and :guilabel:`X509 Certificate` +fields, select the CA and certificate you created in the previous step. + +Under :guilabel:`Configuration`, click on :guilabel:`Configuration Menu`, +then change :guilabel:`Server (Bridged)` to :guilabel:`Server (Routed)`. + +Setting up a Bridged Server is similar to setting up a Routed Server but +is not covered in this tutorial. + +Adjust the rest of the VPN configuration to match the settings in +``/etc/openvpn/server.conf``. + +.. tip:: + + You can verify if your VPN configuration matches the ``server.conf`` + file by using the :guilabel:`Preview Configuration` button at the top + right corner of the page. + +Create the VPN-Client Template in OpenWISP +------------------------------------------ + +In your OpenWISP dashboard, go to ``/admin/config/template/add/``. + +Set the :guilabel:`Type` to :guilabel:`VPN-client`. + +Once the :guilabel:`VPN` field appears, select the VPN you created in the +previous step. + +Ensure the :guilabel:`Automatic tunnel provisioning` flag remains enabled. + +If this template is for your management VPN or the default VPN option, we +recommend checking the :guilabel:`Enabled by default` flag. For more +information about this flag, refer to :ref:`default_templates`. + +Now, save the template. + +After saving the template, you can tweak the VPN Client configuration, +which is automatically generated to be compatible with the server +configuration. + +Finally you can add the new template to your devices. + +.. tip:: + + If you need to troubleshoot any issue, increase the verbosity of the + OpenVPN logging, both on the server and the clients, and check both logs + (on the server and on the client). + +.. seealso:: + + You may also want to explore other automated VPN tunnel provisioning + options: + + - :doc:`Wireguard ` + - :doc:`Wireguard over VXLAN ` + - :doc:`Zerotier ` From 135466ea85c145df18ae9dc0d5b18bb9340f1cc9 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 18:53:50 -0400 Subject: [PATCH 38/44] [docs] Cross reference VPN pages --- docs/user/vxlan-wireguard.rst | 8 ++++++++ docs/user/wireguard.rst | 9 +++++++++ docs/user/zerotier.rst | 8 ++++++++ 3 files changed, 25 insertions(+) diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index aa563f027..6756a88e7 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -107,3 +107,11 @@ as the VPN server configuration in OpenWISP. **Voila!** You have successfully configured OpenWISP to manage VXLAN over WireGuard tunnels for your devices. + +.. seealso:: + + You may also want to explore other automated VPN tunnel provisioning options: + + - :doc:`Wireguard ` + - :doc:`Zerotier ` + - :doc:`OpenVPN ` diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst index c11efbbde..0c6f63347 100644 --- a/docs/user/wireguard.rst +++ b/docs/user/wireguard.rst @@ -109,3 +109,12 @@ configuration in OpenWISP. **Voila!** You have successfully configured OpenWISP to manage WireGuard tunnels for your devices. + + +.. seealso:: + + You may also want to explore other automated VPN tunnel provisioning options: + + - :doc:`Wireguard over VXLAN ` + - :doc:`Zerotier ` + - :doc:`OpenVPN ` diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index e61a52be2..ce164ae47 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -121,3 +121,11 @@ from the `official website `_. **Congratulations!** You've successfully configured OpenWISP to manage ZeroTier tunnels on your devices. + +.. seealso:: + + You may also want to explore other automated VPN tunnel provisioning options: + + - :doc:`Wireguard ` + - :doc:`Wireguard over VXLAN ` + - :doc:`OpenVPN ` From 04b51d5eaa92c60846c0d812d3118d450332c6db Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 18:54:06 -0400 Subject: [PATCH 39/44] [docs] Added zerotier youtube video to zerotier page --- docs/user/zerotier.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index ce164ae47..ffd912257 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -1,6 +1,21 @@ Automating ZeroTier Tunnels =========================== +.. raw:: html + +

+ +

+ Follow the procedure described below to set up `ZeroTier `_ tunnels on your devices. From 677b118ba0d51fca003b00c1beeec2b271f3df94 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 17 Jul 2024 20:53:26 -0400 Subject: [PATCH 40/44] [docs] Reformatted --- CHANGES.rst | 455 ++++++++++++++++------------ CONTRIBUTING.rst | 3 +- README.rst | 96 +++--- docs/developer/extending.rst | 223 ++++++++------ docs/developer/installation.rst | 26 +- docs/developer/utils.rst | 178 ++++++----- docs/index.rst | 7 +- docs/partials/developer-docs.rst | 12 +- docs/partials/shared-object.rst | 10 +- docs/user/device-groups.rst | 104 ++++--- docs/user/import-export.rst | 12 +- docs/user/intro.rst | 91 +++--- docs/user/openvpn.rst | 4 +- docs/user/push-operations.rst | 101 +++--- docs/user/rest-api.rst | 231 +++++++------- docs/user/settings.rst | 359 ++++++++++++---------- docs/user/shell-commands.rst | 70 +++-- docs/user/subnet-division-rules.rst | 143 +++++---- docs/user/templates.rst | 154 +++++----- docs/user/variables.rst | 79 ++--- docs/user/vxlan-wireguard.rst | 110 +++---- docs/user/wireguard.rst | 105 +++---- docs/user/zerotier.rst | 104 ++++--- 23 files changed, 1468 insertions(+), 1209 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 899d22b6d..147c62f34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,22 +23,22 @@ Version 1.0.2 [2022-07-01] Bugfixes ~~~~~~~~ -- Fixed `device's "changed" signals emitting on the creation - of new device `_ -- Fixed *django-reversion's* recovery buttons were hidden from users - of the "Operator" group in the admin dashboard of ``Certificate`` and ``CA`` - models +- Fixed `device's "changed" signals emitting on the creation of new device + `_ +- Fixed *django-reversion's* recovery buttons were hidden from users of + the "Operator" group in the admin dashboard of ``Certificate`` and + ``CA`` models - Removed `hardcoded static URLs - `_ - which created issues when static files are served using an - external service (e.g. S3 storage buckets) -- Fixed `permissions for "Operator" and "Administrator" groups to - access "OrganizationConfigSettings" objects + `_ which + created issues when static files are served using an external service + (e.g. S3 storage buckets) +- Fixed `permissions for "Operator" and "Administrator" groups to access + "OrganizationConfigSettings" objects `_ - Fixed `support for multiple wireguard tunnels on the same devices `_ -- Fixed `"/api/v1/controller/device/{id}/" REST API endpoint not - updating the device's configuration backend +- Fixed `"/api/v1/controller/device/{id}/" REST API endpoint not updating + the device's configuration backend `_ Version 1.0.1 [2022-05-11] @@ -47,17 +47,13 @@ Version 1.0.1 [2022-05-11] Bugfixes ~~~~~~~~ -- Admin: show main group information in ``DeviceGroupAdmin`` list: - - name - - organization - - modified - - created -- Fixed uncaught exception triggered on the deletion of - VPN client certificates +- Admin: show main group information in ``DeviceGroupAdmin`` list: - name + - organization - modified - created +- Fixed uncaught exception triggered on the deletion of VPN client + certificates - SSH connection: fixed OpenWrt <= 19 authentication failure -- The SSH connection is now explicitly closed when - the authentication fails to avoid leaving lingering SSH - connection objects open +- The SSH connection is now explicitly closed when the authentication + fails to avoid leaving lingering SSH connection objects open Version 1.0.0 [2022-04-29] -------------------------- @@ -89,42 +85,43 @@ Features - Added caching for ``DeviceChecksumView`` - Added support for ED25519 SSH keys in ``Credentials`` - Added `Device Groups - `_ - to organize devices of a particular organization -- Configuration push updates now use the SIGUSR1 signal to reload openwisp-config + `_ to + organize devices of a particular organization +- Configuration push updates now use the SIGUSR1 signal to reload + openwisp-config - The device list admin page now allows to search for location address Changes ~~~~~~~ Backward incompatible changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++++++++++++++++++++++++++++++ -- Since django-sortedm2m, the widget we use to implement ordered templates, - clears all the many to many relationships every time it has to make changes, - we had to stop deleting ``VpnClient`` instances related to VPN templates - on ``post_clear`` m2m signals - If you wrote any custom derivative which relies on calls like +- Since django-sortedm2m, the widget we use to implement ordered + templates, clears all the many to many relationships every time it has + to make changes, we had to stop deleting ``VpnClient`` instances related + to VPN templates on ``post_clear`` m2m signals If you wrote any custom + derivative which relies on calls like ``device.config.templates.clear()`` to delete related ``VpnClient`` instances and their x509 certificates, you will have to update your code - to remove all the templates using their primary keys, - instead of using ``clear()`` -- The default behavior for the resolution of conflicting management - IPs between devices of different organizations has been changed; - by default, in this new version, the system assumes it's using only - 1 management tunnel for all the organizations, so different devices - from any organization will not have the same management IP to avoid - conflicts. The old behaviour can be restored by setting + to remove all the templates using their primary keys, instead of using + ``clear()`` +- The default behavior for the resolution of conflicting management IPs + between devices of different organizations has been changed; by default, + in this new version, the system assumes it's using only 1 management + tunnel for all the organizations, so different devices from any + organization will not have the same management IP to avoid conflicts. + The old behaviour can be restored by setting `OPENWISP_CONTROLLER_SHARED_MANAGEMENT_IP_ADDRESS_SPACE `_ to ``False`` -- ``OPENWISP_CONTROLLER_BACKEND_DEVICE_LIST`` has been renamed - to ``OPENWISP_CONTROLLER_CONFIG_BACKEND_FIELD_SHOWN`` +- ``OPENWISP_CONTROLLER_BACKEND_DEVICE_LIST`` has been renamed to + ``OPENWISP_CONTROLLER_CONFIG_BACKEND_FIELD_SHOWN`` - ``Device.check_management_ip_changed`` has been changed to private API ``Device._check_management_ip_changed`` Dependencies -^^^^^^^^^^^^ +++++++++++++ - Dropped support for Python 3.6 - Dropped support for Django 2.2 @@ -151,38 +148,37 @@ Dependencies - Added django-cache-memoize to 0.1 Other changes -^^^^^^^^^^^^^ ++++++++++++++ - `Reworked implementation of config_modified signal `_: - - the signal is now always emitted on templates changes m2m events, - also if ``config.status`` is modified, with the differences that - only post_add and post_remove m2m events are used, while - ``post_clear`` is ignored, which fixes the duplicate signal emission - caused by the implementation of sortedm2m; + - the signal is now always emitted on templates changes m2m events, also + if ``config.status`` is modified, with the differences that only + post_add and post_remove m2m events are used, while ``post_clear`` is + ignored, which fixes the duplicate signal emission caused by the + implementation of sortedm2m; - added ``action`` and ``previous_status`` arguments, which allow to understand where the ``config_modified`` signal is being emitted from, - this allows more advanced usage of the signal by custom implementations + this allows more advanced usage of the signal by custom + implementations -- Context variable follows template order: - If two or more applied templates have "default_values" with the same keys, - then the context variables of the template which comes later in the order - will be used +- Context variable follows template order: If two or more applied + templates have "default_values" with the same keys, then the context + variables of the template which comes later in the order will be used - New credentials created with ``auto_add`` set to ``True`` will get added to the existing devices in a background task. This improves the responsiveness of the web application - Decoupled admin LogEntry from Template model -- Device admin only lists relevant templates, i.e. - templates that are shared or belong to the device's organization +- Device admin only lists relevant templates, i.e. templates that are + shared or belong to the device's organization - Improved UX of `system-defined variables `_ -- Name of ``Vpn``, ``Template`` and ``Credentials`` objects is - unique only within the same organization and within the shared - objects -- Added functionality to configure connection failure reasons - for which the system should not send notifications. - Added ``old_failure_reason`` parameter in +- Name of ``Vpn``, ``Template`` and ``Credentials`` objects is unique only + within the same organization and within the shared objects +- Added functionality to configure connection failure reasons for which + the system should not send notifications. Added ``old_failure_reason`` + parameter in ``openwisp_controller.connection.signals.is_working_changed`` signal - Allowed searching devices using their location address in Device admin. - Removed deprecated ``api/device-location/`` endpoint @@ -192,28 +188,29 @@ Other changes Bugfixes ~~~~~~~~ -- Fixed a bug which caused ``VpnClient`` instances to be recreated every time - the configuration templates of a device were changed, - which caused x590 certificates to be destroyed and recreated as well -- Hardened config validation of OpenVPN backend. The validation fails - if the ``openvpn`` key is missing from the configuration -- Fixed a bug that caused issues in updating related ``Config`` whenever - a template's ``default_values`` were changed +- Fixed a bug which caused ``VpnClient`` instances to be recreated every + time the configuration templates of a device were changed, which caused + x590 certificates to be destroyed and recreated as well +- Hardened config validation of OpenVPN backend. The validation fails if + the ``openvpn`` key is missing from the configuration +- Fixed a bug that caused issues in updating related ``Config`` whenever a + template's ``default_values`` were changed - Fixed pop-up view of CA and Cert not displaying data -- Fixed config status stays ``applied`` after clearing all device templates +- Fixed config status stays ``applied`` after clearing all device + templates - Fixed ``VpnClient`` not created when multiple VPN templates are added -- Fixed configuration editor raising validation error when using variables in - fields with ``maxLength`` set +- Fixed configuration editor raising validation error when using variables + in fields with ``maxLength`` set - Fixed connection notifications reporting outdated status -- Fixed migrations referencing non-swappable OpenWISP modules that - broke OpenWISP's extensibility +- Fixed migrations referencing non-swappable OpenWISP modules that broke + OpenWISP's extensibility - Fixed bugs in restoring deleted devices using ``django-reversion`` - Fixed cloning of shared templates -- Disallowed blank values for ``key_length`` or ``digest`` fields for ``CA`` - and ``Cert`` objects +- Disallowed blank values for ``key_length`` or ``digest`` fields for + ``CA`` and ``Cert`` objects - Fixed template ordering bug in the configuration preview on Device admin - The order of templates was not always retained when - generating the preview of a config object + The order of templates was not always retained when generating the + preview of a config object Version 0.8.4 [2021-04-09] -------------------------- @@ -221,26 +218,34 @@ Version 0.8.4 [2021-04-09] Bugfixes ~~~~~~~~ -- Fixed `bug in connection module `_ - that raised ``UnicodeDecodeError``, improved logging and ignored unicode +- Fixed `bug in connection module + `_ that + raised ``UnicodeDecodeError``, improved logging and ignored unicode conversion issues -- Fixed `context loading from default values of templates overwriting system - defined variables `_ - in device admin -- Fixed `default template selection not updating after changing backend field `_ - in device admin +- Fixed `context loading from default values of templates overwriting + system defined variables + `_ in device + admin +- Fixed `default template selection not updating after changing backend + field `_ in + device admin - Fixed JSONSchema widget to enable working with a single schema -- Fixed `related configuration not getting updated after template "default_values" are changed `_ -- Fixed `bug which caused the unsaved changes alert in device admin `_ - when location of device is present -- Fixed `bug replacing manually entered device information with empty string `_ -- Fixed `multiple requests for fetching default template values in device admin `_ +- Fixed `related configuration not getting updated after template + "default_values" are changed + `_ +- Fixed `bug which caused the unsaved changes alert in device admin + `_ when + location of device is present +- Fixed `bug replacing manually entered device information with empty + string `_ +- Fixed `multiple requests for fetching default template values in device + admin `_ Security ~~~~~~~~ -- Patched security bugs in internal HTTP endpoints which allowed to obtain UUID - of other organizations and other sensitive information +- Patched security bugs in internal HTTP endpoints which allowed to obtain + UUID of other organizations and other sensitive information Version 0.8.3 [2020-12-18] -------------------------- @@ -250,7 +255,8 @@ Bugfixes - Increased minimum `openwisp-users version to ~=0.5.1 `_, - which fixes an `issue in the production setup `_ + which fixes an `issue in the production setup + `_ Version 0.8.2 [2020-12-11] -------------------------- @@ -258,15 +264,17 @@ Version 0.8.2 [2020-12-11] Bugfixes ~~~~~~~~ -- Fixed the `bug `_ - that prevented users from adding/editing access credentials. +- Fixed the `bug + `_ that + prevented users from adding/editing access credentials. Changes ~~~~~~~ -- Increased `django-x509 `_ - version to 0.9.2 -- Increased `django-flat-json-widget `_ +- Increased `django-x509 + `_ version to 0.9.2 +- Increased `django-flat-json-widget + `_ version to 0.1.2 - Changed the `preview` button colors for better readability - Added *help text* for *device name* field @@ -277,7 +285,8 @@ Version 0.8.1 [2020-12-02] Bugfixes ~~~~~~~~ -- Fixed tests that were dependent on specific settings of the Django project. +- Fixed tests that were dependent on specific settings of the Django + project. Version 0.8.0 [2020-11-23] -------------------------- @@ -290,58 +299,80 @@ Features - Added flat JSON widget for configuration variables - Added JSON Schema widget to credentials admin - Added ``device_registered`` signal -- Added `OpenWISP Notifications `_ - module as a dependency, which brings support for - web and email notifications for important events -- Allow using a different device model in update_config: - his allows `OpenWISP Monitoring `_ - to override the ``can_be_updated`` method to take into account the monitoring status, - so that push updates won't be attempted +- Added `OpenWISP Notifications + `_ + module as a dependency, which brings support for web and email + notifications for important events +- Allow using a different device model in update_config: his allows + `OpenWISP Monitoring + `_ + to override the ``can_be_updated`` method to take into account the + monitoring status, so that push updates won't be attempted - Added notifications for changes of ``is_working`` status of credentials - UX, automatically add/remove default values to device context: - automatically add or remove default values of templates to the configuration context - (a.k.a. configuration variables) when templates are added or removed from devices + automatically add or remove default values of templates to the + configuration context (a.k.a. configuration variables) when templates + are added or removed from devices - UX: added `system defined variables - `_ section + `_ + section Changes ~~~~~~~ -- **Backward incompatible**: the code of `django-netjsonconfig `_ - was merged in openwisp-controller to simplify maintenance -- Changed API of ``device_location`` view for consistency: ``/api/device-location/{id}/`` - becomes ``/api/v1/device/{id}/location/``, the old URL is kept for backward compatibility - but will be removed in the future -- **Backward incompatible change**: schema url endpoint changed to ``/config/schema.json`` - and it's now in config namespace instead of admin namespace -- Changed VPN DH length to 2048 and move its generation to the background because it's a lot slower +- **Backward incompatible**: the code of `django-netjsonconfig + `_ was merged in + openwisp-controller to simplify maintenance +- Changed API of ``device_location`` view for consistency: + ``/api/device-location/{id}/`` becomes + ``/api/v1/device/{id}/location/``, the old URL is kept for backward + compatibility but will be removed in the future +- **Backward incompatible change**: schema url endpoint changed to + ``/config/schema.json`` and it's now in config namespace + instead of admin namespace +- Changed VPN DH length to 2048 and move its generation to the background + because it's a lot slower - Admin: Order Device, Template and VPN alphabetically by default -- Admin: Added ``mac_address`` field to the device list page (``DeviceAdmin.list_display``) +- Admin: Added ``mac_address`` field to the device list page + (``DeviceAdmin.list_display``) - Increased ``max_length`` of common name to ``64`` - Changed the config apply logic to avoid restarting the openwisp-config deamon if the configuration apply procedure is already being run - Made template ``config`` field required in most cases -- Changed ``DeviceConnection.failure_reason`` field to ``TextField``, - this avoids possible exception if ``failed_reason`` is very long, - which may happen in some corner cases -- Made Device ``verbose_name`` configurable, see ``OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME`` -- Increased `netjsonconfig `__ version to 0.9.x - (which brings support for new interface types, - `see the change log of netjsonconfig `_ +- Changed ``DeviceConnection.failure_reason`` field to ``TextField``, this + avoids possible exception if ``failed_reason`` is very long, which may + happen in some corner cases +- Made Device ``verbose_name`` configurable, see + ``OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME`` +- Increased `netjsonconfig + `__ version to + 0.9.x (which brings support for new interface types, `see the change log + of netjsonconfig + `_ for more information) -- Increased `django-x509 `_ version to 0.9.x -- Increased `django-loci `_ version to 0.4.x - (which brings many bug fixes to the mapping feature, as long as support for - geo-coding and reverse geo-coding, - `see the change log of django-loci `_ +- Increased `django-x509 + `_ version to 0.9.x +- Increased `django-loci + `_ version to 0.4.x + (which brings many bug fixes to the mapping feature, as long as support + for geo-coding and reverse geo-coding, `see the change log of + django-loci + `_ for more information) -- Increased `openwisp-users `__ version from 0.2.x to 0.5.x - (which brings many interesting improvements to multi-tenancy, - `see the change log of openwisp-users `_ +- Increased `openwisp-users + `__ version + from 0.2.x to 0.5.x (which brings many interesting improvements to + multi-tenancy, `see the change log of openwisp-users + `_ for more information) -- Increased `django-taggit `_ version to 1.3.x -- Increased `openwisp-utils `__ version to 0.7.x -- Increased `django-rest-framework-gis `_ version to 0.16.x +- Increased `django-taggit `_ + version to 1.3.x +- Increased `openwisp-utils + `__ version + to 0.7.x +- Increased `django-rest-framework-gis + `_ version to + 0.16.x - Added support for django 3.1 Bugfixes @@ -356,31 +387,33 @@ Bugfixes - Avoid triggering ``config_modified`` signal during registration - UI: Fixed whitespace after overview tab in in device page - Validate ``Config.context`` and ``Template.default_values``: - ``Config.context`` and ``Template.default_values`` must always be a dictionary, - falsy values will be converted to empty dictionary automatically -- Fixed failures in ``update_config`` operation: - the ``update_config`` operation will be executed only when the transaction - is committed to the database; also handled rare but possible error conditions + ``Config.context`` and ``Template.default_values`` must always be a + dictionary, falsy values will be converted to empty dictionary + automatically +- Fixed failures in ``update_config`` operation: the ``update_config`` + operation will be executed only when the transaction is committed to the + database; also handled rare but possible error conditions - Handled device not existing case in ``update_config`` task - Fixed auto cert feature failure when device name is too long - UI: avoid showing main scrollbar in preview mode - Fixed ``OPENWISP_CONTROLLER_BACKEND_DEVICE_LIST = False`` -- UI fixed advanced mode bugs: positioning is done using css instead of js. - Removed body scrollbar when in advanced mode. - Back to normal mode with ESC key. - Hidden netjsonconfig docs hint on narrow screens. -- Avoid simultaneous ``update_config`` tasks: - since now the launch of the task is executed when the - transaction is committed to the database, also the - check for other updates in progress must be moved there -- Fixed ``OPENWISP_CONTROLLER_CONTEXT`` setting getting modified at run time -- Fixed z-index of preview overlay: the z-index is increased so it's higher - than the main navigation menu to avoid the possibility of triggering the - main menu inadvertently +- UI fixed advanced mode bugs: positioning is done using css instead of + js. Removed body scrollbar when in advanced mode. Back to normal mode + with ESC key. Hidden netjsonconfig docs hint on narrow screens. +- Avoid simultaneous ``update_config`` tasks: since now the launch of the + task is executed when the transaction is committed to the database, also + the check for other updates in progress must be moved there +- Fixed ``OPENWISP_CONTROLLER_CONTEXT`` setting getting modified at run + time +- Fixed z-index of preview overlay: the z-index is increased so it's + higher than the main navigation menu to avoid the possibility of + triggering the main menu inadvertently - Prevent sending ``config_modified`` signal multiple times -- Fix timeout when changing template: slow operations are moved to the background -- Fixed variablle validation: now all the available context - (device variables, system variables) are taken into account when performing validation +- Fix timeout when changing template: slow operations are moved to the + background +- Fixed variablle validation: now all the available context (device + variables, system variables) are taken into account when performing + validation - Removed unnecessary ``static()`` call from media assets Version 0.7.0.post1 [2020-07-01] @@ -391,30 +424,39 @@ Version 0.7.0.post1 [2020-07-01] Version 0.7.0 [2020-07-01] -------------------------- -- [feature] Added signals: ``config_status_changed``, ``checksum_requested``, ``config_download_requested`` -- [feature] Added the possibility of specifying default values for variables used in templates +- [feature] Added signals: ``config_status_changed``, + ``checksum_requested``, ``config_download_requested`` +- [feature] Added the possibility of specifying default values for + variables used in templates - [feature] Added ``banner_timeout`` - [feature] Emit signal when ``DeviceConnection.is_working`` changes - [change] **Backward incompatible change**: the ``config_modified`` signal is not emitted anymore when the device is created - [change] VPN files now have 0600 permissions by default -- [change] Increased minimum `netjsonconfig `_ version to 0.8.0 -- [change] Increased minimum `paramiko `_ version to 2.7.1 -- [change] Increased minimum `celery `_ version to 4.4.3 +- [change] Increased minimum `netjsonconfig + `_ version to 0.8.0 +- [change] Increased minimum `paramiko + `_ version to 2.7.1 +- [change] Increased minimum `celery `_ + version to 4.4.3 - [fix] Avoid errors being hidden by tabs - [fix] Fixed clashes between javascript schema validation and variables - [fix] Fixed exception when adding device credential without type -- [fix] Fixed exception when auto adding device credentials to devices which don't have a configuration -- [fix] Avoid multiple devices having the same management IP address (multiple devices - having the same last IP is allowed because last IP is almost always a public address) +- [fix] Fixed exception when auto adding device credentials to devices + which don't have a configuration +- [fix] Avoid multiple devices having the same management IP address + (multiple devices having the same last IP is allowed because last IP is + almost always a public address) - [docs] Documented SSH timeouts - [docs] Update outdated steps in README instructions Version 0.6.0 [2020-04-02] -------------------------- -- Added controller view that allows to update the device information (firmware version used) -- Recover deleted object views in recoverable objects now show latest objects first +- Added controller view that allows to update the device information + (firmware version used) +- Recover deleted object views in recoverable objects now show latest + objects first - Added ``NETJSONCONFIG_HARDWARE_ID_AS_NAME`` setting Version 0.5.2 [2020-03-18] @@ -422,21 +464,23 @@ Version 0.5.2 [2020-03-18] - [controller] Added ``NETJSONCONFIG_REGISTRATION_SELF_CREATION`` - [models] Handled accidental duplication of files across templates -- [controller] Update hardware device info during registration - (if the device already exists, the registration will update its info) +- [controller] Update hardware device info during registration (if the + device already exists, the registration will update its info) - [admin] Moved ``hardware_id`` field in device list admin - [bugfix] Fixed broken preview when using ``hardware_id`` context var -- [models] Flagged ``hardware_id`` as not unique (it's ``unique_together`` with ``organization``) +- [models] Flagged ``hardware_id`` as not unique (it's ``unique_together`` + with ``organization``) - [admin] Hidden device configuration context field into advanced options - [models] Removed LEDE from the OpenWRT backend label -- [docker] Added ``REDIS_URL`` to docker-compose.yml and settings.py (for dev and test env) +- [docker] Added ``REDIS_URL`` to docker-compose.yml and settings.py (for + dev and test env) Version 0.5.1 [2020-02-28] -------------------------- -- [models] Improved consistent key generation, now a consisten key is generated - also when creating devices from the admin interface (or via model API), - before it was only done during registration +- [models] Improved consistent key generation, now a consisten key is + generated also when creating devices from the admin interface (or via + model API), before it was only done during registration - [admin] Fixed unsaved changes JS bug that was triggered in certain cases - [deps] Switched back to jsonfield @@ -446,8 +490,8 @@ Version 0.5.0 [2020-02-05] - [deps] Upgraded to django 3, upgraded dependencies - [deps] Dropped support for python 2 - [x509] Fixed serial number max length (imported from django-x509) -- [admin] Fixed bug that caused organization field to be missing - when importing a CA or certificate +- [admin] Fixed bug that caused organization field to be missing when + importing a CA or certificate Version 0.4.0 [2020-01-09] -------------------------- @@ -455,11 +499,13 @@ Version 0.4.0 [2020-01-09] - [feature] Added connection module (possibility to SSH into devices) - [feature] Added default operator group - [feature] Added management IP feature -- [change] Changed configuration status: ``running`` has been renamed to ``applied`` +- [change] Changed configuration status: ``running`` has been renamed to + ``applied`` - [admin] Added ``NETJSONCONFIG_MANAGEMENT_IP_DEVICE_LIST`` setting - [admin] Added ``NETJSONCONFIG_BACKEND_DEVICE_LIST`` setting - [x509] Fixed common_name redundancy -- [admin] Hidden "Download Configuration" button when no config is available +- [admin] Hidden "Download Configuration" button when no config is + available - [controller] Register view now updates device details - [deps] Added support for Django 2.1 and Django 2.2 - [models] Added support for hardware ID / serial number @@ -472,7 +518,8 @@ Version 0.4.0 [2020-01-09] - [change] Moved urls to admin namespace - [feature] Implement copy/clone templates - [feature] Added API to get context of device -- [bugfix] Ensure atomicity of transactions with database during auto-registration +- [bugfix] Ensure atomicity of transactions with database during + auto-registration Version 0.3.2 [2018-02-19] -------------------------- @@ -506,29 +553,36 @@ Version 0.2.4 [2017-11-07] Version 0.2.3 [2017-08-29] -------------------------- -- `934be13 `_: +- `934be13 + `_: [models] Updated sortedm2m __str__ definition -- `b76e4e2 `_: +- `b76e4e2 + `_: [requirements] django-netjsonconfig>=0.6.3,<0.7.0 Version 0.2.2 [2017-07-10] -------------------------- -- `f3dc784 `_: +- `f3dc784 + `_: [admin] Moved ``submit_line.html`` to `openwisp-utils `_ Version 0.2.1 [2017-07-05] -------------------------- -- `0064b98 `_: +- `0064b98 + `_: [device] Added ``system`` field -- `c7fe513 `_: +- `c7fe513 + `_: [docs] Added "Installing for development" section to README -- `c75fa68 `_: +- `c75fa68 + `_: [openwisp-utils] Moved shared logic to `openwisp-utils `_ -- `819cb21 `_: +- `819cb21 + `_: [requirements] django-netjsonconfig>=0.6.2,<0.7.0 Version 0.2.0 [2017-05-24] @@ -540,13 +594,17 @@ Version 0.2.0 [2017-05-24] [feature] Added ``Device`` model - `#9 `_: [admin] Load default templates JS logic only when required -- `298b2a2 `_: +- `298b2a2 + `_: [admin] Avoid setting ``extra_content`` to mutable object -- `d173c24 `_: +- `d173c24 + `_: [migrations] Squashed ``0001`` and ``0002`` to avoid postgres error -- `f5fb628 `_: +- `f5fb628 + `_: [migrations] Updated indexes -- `6200b7a `_: +- `6200b7a + `_: [Template] Fixed ``auto_client`` bug Version 0.1.4 [2017-04-21] @@ -558,25 +616,32 @@ Version 0.1.4 [2017-04-21] Version 0.1.3 [2017-03-11] -------------------------- -- `db77ae7 `_: +- `db77ae7 + `_: [controller] Added "error: " prefix in error responses Version 0.1.2 [2017-03-15] -------------------------- -- `3c61053 `_: +- `3c61053 + `_: [admin] Ensure preview button is present -- `0087483 `_: +- `0087483 + `_: [models] Converted ``OrganizationConfigSettings`` to UUID primary key Version 0.1.1 [2017-03-10] -------------------------- -- `cbca4e1 `_: - [users] Fixed integration with `openwisp-users `_ +- `cbca4e1 + `_: + [users] Fixed integration with `openwisp-users + `_ Version 0.1.0 [2017-03-08] -------------------------- -- added multi-tenancy (separation of organizations) to `openwisp2 `_ -- added email confirmation of new users (via `django-allauth `_) +- added multi-tenancy (separation of organizations) to `openwisp2 + `_ +- added email confirmation of new users (via `django-allauth + `_) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 79c907b8c..571f60f47 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 4d819cb6e..8ebac16da 100644 --- a/README.rst +++ b/README.rst @@ -2,78 +2,81 @@ openwisp-controller =================== .. image:: https://github.com/openwisp/openwisp-controller/workflows/OpenWISP%20Controller%20CI%20Build/badge.svg?branch=master - :target: https://github.com/openwisp/openwisp-controller/actions?query=workflow%3A%22OpenWISP+Controller+CI+Build%22 - :alt: CI build status + :target: https://github.com/openwisp/openwisp-controller/actions?query=workflow%3A%22OpenWISP+Controller+CI+Build%22 + :alt: CI build status .. image:: https://coveralls.io/repos/openwisp/openwisp-controller/badge.svg - :target: https://coveralls.io/r/openwisp/openwisp-controller - :alt: Test Coverage + :target: https://coveralls.io/r/openwisp/openwisp-controller + :alt: Test Coverage .. image:: https://img.shields.io/librariesio/release/github/openwisp/openwisp-controller - :target: https://libraries.io/github/openwisp/openwisp-controller#repository_dependencies - :alt: Dependency monitoring + :target: https://libraries.io/github/openwisp/openwisp-controller#repository_dependencies + :alt: Dependency monitoring .. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg - :target: https://gitter.im/openwisp/general - :alt: Chat + :target: https://gitter.im/openwisp/general + :alt: Chat .. image:: https://badge.fury.io/py/openwisp-controller.svg - :target: http://badge.fury.io/py/openwisp-controller - :alt: Pypi Version + :target: http://badge.fury.io/py/openwisp-controller + :alt: Pypi Version .. image:: https://pepy.tech/badge/openwisp-controller - :target: https://pepy.tech/project/openwisp-controller - :alt: Downloads + :target: https://pepy.tech/project/openwisp-controller + :alt: Downloads .. 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://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/controller_demo.gif - :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/controller_demo.gif - :alt: Feature Highlights + :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/controller_demo.gif + :alt: Feature Highlights ------------- +---- -**Need a quick overview?** `Try the OpenWISP Demo `_. +**Need a quick overview?** `Try the OpenWISP Demo +`_. -OpenWISP Controller is a configuration manager that allows to automate several -networking tasks like adoption, provisioning, management VPN configuration, -X509 certificates automatic generation, revocation of x509 certificates and -a lot more features. +OpenWISP Controller is a configuration manager that allows to automate +several networking tasks like adoption, provisioning, management VPN +configuration, X509 certificates automatic generation, revocation of x509 +certificates and a lot more 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-monitoring `_: - provides device status monitoring, collection of metrics, charts, alerts, - possibility to define custom checks -- `openwisp-firmware-upgrader `_: - automated firmware upgrades (single devices or mass network upgrades) + provides device status monitoring, collection of metrics, charts, + alerts, possibility to define custom checks +- `openwisp-firmware-upgrader + `_: automated firmware + upgrades (single devices 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-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-ipam `_: - allows to manage the assignment of IP addresses used in the network + based on FreeRADIUS, allows to implement network access authentication + systems like 802.1x WPA2 Enterprise, captive portal authentication, + Hotspot 2.0 (802.11u) +- `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-ipam `_: allows to manage + the assignment of IP addresses used in the network - `openwisp-notifications `_: allows users to be aware of important events happening in the network. **For a more complete overview of the OpenWISP modules and architecture**, -see the -`OpenWISP Architecture Overview +see the `OpenWISP Architecture Overview `_. .. image:: https://raw.githubusercontent.com/openwisp/openwisp2-docs/master/assets/design/openwisp-logo-black.svg - :target: http://openwisp.org - :alt: OpenWISP + :target: http://openwisp.org + :alt: OpenWISP **Want to help OpenWISP?** `Find out how to help us grow here `_. @@ -87,17 +90,20 @@ Documentation Contributing ------------ -Please refer to the `OpenWISP contributing guidelines `_. +Please refer to the `OpenWISP contributing guidelines +`_. Changelog --------- -See `CHANGES `_. +See `CHANGES +`_. License ------- -See `LICENSE `_. +See `LICENSE +`_. Support ------- diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 051d081a3..599d81f85 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -3,22 +3,23 @@ Extending OpenWISP Controller .. include:: ../partials/developer-docs.rst -One of the core values of the OpenWISP project is :ref:`Software Reusability `, -for this reason *OpenWISP Controller* 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 +Controller* 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 Controller*, you need to perform -the steps described in this section. +In order to implement your custom version of *OpenWISP Controller*, you +need to perform the steps described in this section. When in doubt, the code in the `test project -`_ will -serve you as source of truth: just replicate and adapt that code to get a basic -derivative of *OpenWISP Controller* working. - -If you want to add new users fields, please follow the :doc:`tutorial to extend the -openwisp-users module `. As an example, we have extended -*openwisp-users* to *sample_users* app and added a field ``social_security_number`` in -the `sample_users/models.py +`_ +will serve you as source of truth: just replicate and adapt that code to +get a basic derivative of *OpenWISP Controller* working. + +If you want to add new users fields, please follow the :doc:`tutorial to +extend the openwisp-users module `. As an +example, we have extended *openwisp-users* to *sample_users* app and added +a field ``social_security_number`` in the `sample_users/models.py `_. .. important:: @@ -40,18 +41,19 @@ Firstly, to get started you need to create a django project: django-admin startproject mycontroller -Now, you need to do is to create some new django apps which will contain your custom -version of *OpenWISP Controller*. +Now, you need to do is to create some new django apps which will contain +your custom version of *OpenWISP Controller*. -A django project is a collection of django apps. There are 4 django apps in the -openwisp_controller project, namely config, pki, connection & geo. You'll need to create -4 apps in your project for each app in openwisp_controller. +A django project is a collection of django apps. There are 4 django apps +in the openwisp_controller project, namely config, pki, connection & geo. +You'll need to create 4 apps in your project for each app in +openwisp_controller. A django app is nothing more than a `python package -`_ (a directory of python -scripts), in the following examples we'll call these django app ``sample_config``, -``sample_pki``, ``sample_connection``, ``sample_geo`` & ``sample_subnet_division``. but -you can name it how you want: +`_ (a directory +of python scripts), in the following examples we'll call these django app +``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` & +``sample_subnet_division``. but you can name it how you want: .. code-block:: @@ -61,13 +63,13 @@ you can name it how you want: django-admin startapp sample_geo django-admin startapp sample_subnet_division -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. -For more information about how to work with django projects and django apps, please -refer to the `django documentation +For more information about how to work with django projects and django +apps, please refer to the `django documentation `_. 2. Install ``openwisp-controller`` @@ -82,10 +84,11 @@ Install (and add to the requirement of your project) openwisp-controller: 3. Add Your Apps to ``INSTALLED_APPS`` -------------------------------------- -Now you need to add ``mycontroller.sample_config``, ``mycontroller.sample_pki``, -``mycontroller.sample_connection``, ``mycontroller.sample_geo`` & -``mycontroller.sample_subnet_division`` to ``INSTALLED_APPS`` in your ``settings.py``, -ensuring also that ``openwisp_controller.config``, ``openwisp_controller.geo``, +Now you need to add ``mycontroller.sample_config``, +``mycontroller.sample_pki``, ``mycontroller.sample_connection``, +``mycontroller.sample_geo`` & ``mycontroller.sample_subnet_division`` to +``INSTALLED_APPS`` in your ``settings.py``, ensuring also that +``openwisp_controller.config``, ``openwisp_controller.geo``, ``openwisp_controller.pki``, ``openwisp_controller.connnection`` & ``openwisp_controller.subnet_division`` have been removed: @@ -128,8 +131,9 @@ ensuring also that ``openwisp_controller.config``, ``openwisp_controller.geo``, "import_export", ] -Substitute ``mycontroller``, ``sample_config``, ``sample_pki``, ``sample_connection``, -``sample_geo`` & ``sample_subnet_division`` with the name you chose in step 1. +Substitute ``mycontroller``, ``sample_config``, ``sample_pki``, +``sample_connection``, ``sample_geo`` & ``sample_subnet_division`` with +the name you chose in step 1. 4. Add ``EXTENDED_APPS`` ------------------------ @@ -151,8 +155,8 @@ Add the following to your ``settings.py``: 5. Add ``openwisp_utils.staticfiles.DependencyFinder`` ------------------------------------------------------ -Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` in your -``settings.py``: +Add ``openwisp_utils.staticfiles.DependencyFinder`` to +``STATICFILES_FINDERS`` in your ``settings.py``: .. code-block:: python @@ -206,8 +210,8 @@ Ensure you are using one of the available geodjango backends, eg: } } -For more information about GeoDjango, please refer to the `geodjango documentation -`_. +For more information about GeoDjango, please refer to the `geodjango +documentation `_. 6. Django Channels Setup ------------------------ @@ -249,13 +253,14 @@ Add the following settings to ``settings.py``: "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}, } -For more information about FORM_RENDERER setting, please refer to the `FORM_RENDERER -documentation `_. For -more information about ASGI_APPLICATION setting, please refer to the `ASGI_APPLICATION -documentation +For more information about FORM_RENDERER setting, please refer to the +`FORM_RENDERER documentation +`_. For +more information about ASGI_APPLICATION setting, please refer to the +`ASGI_APPLICATION documentation `_. -For more information about CHANNEL_LAYERS setting, please refer to the `CHANNEL_LAYERS -documentation +For more information about CHANNEL_LAYERS setting, please refer to the +`CHANNEL_LAYERS documentation `_. 6. Inherit the AppConfig Class @@ -291,15 +296,15 @@ Please refer to the following files in the sample app of the test project: You have to replicate and adapt that code in your project. -For more information regarding the concept of ``AppConfig`` please refer to the -`"Applications" section in the django documentation +For more information regarding the concept of ``AppConfig`` please refer +to the `"Applications" section in the django documentation `_. 7. Create Your Custom Models ---------------------------- -For the purpose of showing an example, we added a simple "details" field to the models -of the sample app in the test project. +For the purpose of showing an example, we added a simple "details" field +to the models of the sample app in the test project. - `sample_config models `_ @@ -316,14 +321,15 @@ You can add fields in a similar way in your ``models.py`` file. .. note:: - If you have any doubt regarding how to use, extend or develop models please refer to - the `"Models" section in the django documentation + If you have any doubt regarding how to use, extend or develop models + please refer to the `"Models" section in the django documentation `_. 8. Add Swapper Configurations ----------------------------- -Once you have created the models, add the following to your ``settings.py``: +Once you have created the models, add the following to your +``settings.py``: .. code-block:: python @@ -336,7 +342,9 @@ Once you have created the models, add the following to your ``settings.py``: CONFIG_TEMPLATE_MODEL = "sample_config.Template" CONFIG_VPN_MODEL = "sample_config.Vpn" CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient" - CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings" + CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = ( + "sample_config.OrganizationConfigSettings" + ) CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits" DJANGO_X509_CA_MODEL = "sample_pki.Ca" DJANGO_X509_CERT_MODEL = "sample_pki.Cert" @@ -346,11 +354,16 @@ Once you have created the models, add the following to your ``settings.py``: CONNECTION_CREDENTIALS_MODEL = "sample_connection.Credentials" CONNECTION_DEVICECONNECTION_MODEL = "sample_connection.DeviceConnection" CONNECTION_COMMAND_MODEL = "sample_connection.Command" - SUBNET_DIVISION_SUBNETDIVISIONRULE_MODEL = "sample_subnet_division.SubnetDivisionRule" - SUBNET_DIVISION_SUBNETDIVISIONINDEX_MODEL = "sample_subnet_division.SubnetDivisionIndex" + SUBNET_DIVISION_SUBNETDIVISIONRULE_MODEL = ( + "sample_subnet_division.SubnetDivisionRule" + ) + SUBNET_DIVISION_SUBNETDIVISIONINDEX_MODEL = ( + "sample_subnet_division.SubnetDivisionIndex" + ) -Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, ``sample_geo`` & -``sample_subnet_division`` with the name you chose in step 1. +Substitute ``sample_config``, ``sample_pki``, ``sample_connection``, +``sample_geo`` & ``sample_subnet_division`` with the name you chose in +step 1. 9. Create Database Migrations ----------------------------- @@ -361,9 +374,9 @@ Create database migrations: ./manage.py makemigrations -Now, to use the default ``administrator`` and ``operator`` user groups like the used in -the openwisp_controller module, you'll manually need to make a migrations file which -would look like: +Now, to use the default ``administrator`` and ``operator`` user groups +like the used in the openwisp_controller module, you'll manually need to +make a migrations file which would look like: - `sample_config/migrations/0002_default_groups_permissions.py `_ @@ -382,7 +395,8 @@ Create database migrations: ./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 the Admin @@ -401,17 +415,19 @@ Refer to the ``admin.py`` file of the sample app. - `sample_subnet_division admin.py `_. -To introduce changes to the admin, you can do it in two main ways which are described -below. +To introduce changes to the admin, you can do it in two main ways which +are described below. -**Note**: for more information regarding how the django admin works, or how it can be -customized, please refer to `"The django admin site" section in the django documentation +**Note**: for more information 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: @@ -461,15 +477,19 @@ For example: .. code-block:: python - from openwisp_controller.subnet_division.admin import SubnetDivisionRuleInlineAdmin + from openwisp_controller.subnet_division.admin import ( + SubnetDivisionRuleInlineAdmin, + ) - SubnetDivisionRuleInlineAdmin.fields += ["example"] # <-- monkey patching example + SubnetDivisionRuleInlineAdmin.fields += [ + "example" + ] # <-- monkey patching example 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: ``sample_config`` +++++++++++++++++ @@ -659,24 +679,29 @@ patching, you can proceed as follows: # url(r'^geo/', include((get_geo_urls(geo_views), 'geo'), namespace='geo')), # openwisp-controller urls url( - r"", include(("openwisp_controller.config.urls", "config"), namespace="config") + r"", + include( + ("openwisp_controller.config.urls", "config"), + namespace="config", + ), ), url(r"", include("openwisp_controller.urls")), ] -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. 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 Controller*. +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 Controller*. -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. See the tests in sample_app to find out how to do this. @@ -697,7 +722,8 @@ See the tests in sample_app to find out how to do this. For running the tests, you need to copy fixtures as well: -- Change `sample_config` to your config app's name in `sample_config fixtures +- Change `sample_config` to your config app's name in `sample_config + fixtures `_ and paste it in the ``sample_config/fixtures/`` directory. @@ -710,35 +736,41 @@ You can then run tests with: Substitute ``mycontroller`` with the name you chose in step 1. -For more information about automated tests in django, please refer to `"Testing in -Django" `_. +For more information about automated tests in django, please refer to +`"Testing in Django" +`_. 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. 1. Extending the Controller API Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Extending the `sample_config/views.py `_ -is required only when you want to make changes in the controller API, Remember to change -``config_views`` location in ``urls.py`` in point 11 for extending views. +is required only when you want to make changes in the controller API, +Remember to change ``config_views`` location in ``urls.py`` in point 11 +for extending views. -For more information about django views, please refer to the `views section in the -django documentation `_. +For more information about django views, please refer to the `views +section in the django documentation +`_. 2. Extending the Geo API Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Extending the `sample_geo/views.py `_ -is required only when you want to make changes in the geo API, Remember to change -``geo_views`` location in ``urls.py`` in point 11 for extending views. +is required only when you want to make changes in the geo API, Remember to +change ``geo_views`` location in ``urls.py`` in point 11 for extending +views. -For more information about django views, please refer to the `views section in the -django documentation `_. +For more information about django views, please refer to the `views +section in the django documentation +`_. .. _custom_subnet_division_rule_types: @@ -746,14 +778,15 @@ Custom Subnet Division Rule Types --------------------------------- It is possible to create your own :doc:`subnet division rule types -<../user/subnet-division-rules>`. The rule type determines when subnets and IPs will be -provisioned and when they will be destroyed. +<../user/subnet-division-rules>`. The rule type determines when subnets +and IPs will be provisioned and when they will be destroyed. You can create your custom rule types by extending ``openwisp_controller.subnet_division.rule_types.base.BaseSubnetDivisionRuleType``. -Below is an example to create a subnet division rule type that will provision subnets -and IPs when a new device is created and will delete them upon deletion for that device. +Below is an example to create a subnet division rule type that will +provision subnets and IPs when a new device is created and will delete +them upon deletion for that device. .. code-block:: python @@ -808,7 +841,9 @@ and IPs when a new device is created and will delete them upon deletion for that # performs no operation for existing objects. @classmethod def provision_for_existing_objects(cls, rule_obj): - for device in Device.objects.filter(organization=rule_obj.organization): + for device in Device.objects.filter( + organization=rule_obj.organization + ): cls.provision_receiver(device, created=True) After creating a class for your custom rule type, you will need to set diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst index 2761e9b77..45b598cfe 100644 --- a/docs/developer/installation.rst +++ b/docs/developer/installation.rst @@ -51,7 +51,8 @@ Setup and activate a virtual-environment (we'll be using `virtualenv python -m virtualenv env source env/bin/activate -Make sure that your base python packages are up to date before moving to the next step: +Make sure that your base python packages are up to date before moving to +the next step: .. code-block:: shell @@ -66,8 +67,8 @@ Install development dependencies: sudo npm install -g jshint stylelint 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/``). +https://chromedriver.chromium.org/home and Extract ``chromedriver`` to one +of directories from your ``$PATH`` (example: ``~/.local/bin/``). Create database: @@ -97,10 +98,11 @@ Run tests with: ./runtests.py --parallel -Some tests, such as the Selenium UI tests, require a PostgreSQL database to run. If you -don't have a PostgreSQL database running on your system, you can use :ref:`the Docker -Compose configuration provided in this repository `. Once set up, -you can run these specific tests as follows: +Some tests, such as the Selenium UI tests, require a PostgreSQL database +to run. If you don't have a PostgreSQL database running on your system, +you can use :ref:`the Docker Compose configuration provided in this +repository `. Once set up, you can run these +specific tests as follows: .. code-block:: shell @@ -182,8 +184,9 @@ If you are incurring in the following exception: django.core.exceptions.ImproperlyConfigured: Unable to load the SpatiaLite library extension -You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as explained in -`django documentation regarding how to install and configure spatialte +You need to specify ``SPATIALITE_LIBRARY_PATH`` in your ``settings.py`` as +explained in `django documentation regarding how to install and configure +spatialte `_. Having Issues with Other Geospatial Libraries? @@ -194,6 +197,7 @@ Please refer `troubleshooting issues related to geospatial libraries .. important:: - If you want to add OpenWISP Controller to an existing Django project, then you can - refer to the `test project in the openwisp-controller repository + If you want to add OpenWISP Controller to an existing Django project, + then you can refer to the `test project in the openwisp-controller + repository `_. diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst index 4d0a0d881..53b9d1f9b 100644 --- a/docs/developer/utils.rst +++ b/docs/developer/utils.rst @@ -12,34 +12,34 @@ Code Utilities Registering / Unregistering Commands ------------------------------------ -OpenWISP Controller allows to register new command options or unregister existing -command options through two utility functions: +OpenWISP Controller allows to register new command options or unregister +existing command options through two utility functions: - ``openwisp_controller.connection.commands.register_command`` - ``openwisp_controller.connection.commands.unregister_command`` -You can use these functions to register new custom commands or unregister existing -commands from your code. +You can use these functions to register new custom commands or unregister +existing commands from your code. .. note:: These functions are to be used as an alternative to the :ref:`OPENWISP_CONTROLLER_USER_COMMANDS` setting when :doc:`extending - openwisp-controller ` or when developing custom applications based on - OpenWISP Controller. + openwisp-controller ` or when developing custom + applications based on OpenWISP Controller. ``register_command`` ~~~~~~~~~~~~~~~~~~~~ -================== ==================================================================== +================== ============================================== Parameter Description ``command_name`` A ``str`` defining identifier for the command. -``command_config`` A ``dict`` like the one shown in :ref:`Command Configuration: schema - `. -================== ==================================================================== +``command_config`` A ``dict`` like the one shown in :ref:`Command + Configuration: schema `. +================== ============================================== -**Note:** It will raise ``ImproperlyConfigured`` exception if a command is already -registered with the same name. +**Note:** It will raise ``ImproperlyConfigured`` exception if a command is +already registered with the same name. ``unregister_command`` ~~~~~~~~~~~~~~~~~~~~~~ @@ -49,34 +49,36 @@ Parameter Description ``command_name`` A ``str`` defining name of the command. ================ ======================================= -**Note:** It will raise ``ImproperlyConfigured`` exception if such command does not -exists. +**Note:** It will raise ``ImproperlyConfigured`` exception if such command +does not exists. Controller Notifications ------------------------ -The notification types registered and used by OpenWISP Controller are listed in the -following table. +The notification types registered and used by OpenWISP Controller are +listed in the following table. -===================== ========================================================== +===================== =============================================== Notification Type Use -``config_error`` Fires when the status of a device configuration changes to - ``error``. +``config_error`` Fires when the status of a device configuration + changes to ``error``. ``device_registered`` Fires when a new device registers itself. -===================== ========================================================== +===================== =============================================== Registering Notification Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can define your own notification types using ``register_notification_type`` function -from OpenWISP Notifications. +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 `. +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 can use the :doc:`"notify" signal -provided by the Notifications module ` to -send notifications with this new type. +Once a new notification type is registered, you can use the :doc:`"notify" +signal provided by the Notifications module +` to send notifications with +this new type. Signals ------- @@ -91,37 +93,41 @@ Signals **Arguments**: - ``instance``: instance of ``Config`` which got its ``config`` modified -- ``previous_status``: indicates the status of the config object before the signal was - emitted -- ``action``: action which emitted the signal, can be any of the list below: - - ``config_changed``: the configuration of the config object was changed - - ``related_template_changed``: the configuration of a related template was changed - - ``m2m_templates_changed``: the assigned templates were changed (either templates were - added, removed or their order was changed) +- ``previous_status``: indicates the status of the config object before + the signal was emitted +- ``action``: action which emitted the signal, can be any of the list + below: - ``config_changed``: the configuration of the config object was + changed - ``related_template_changed``: the configuration of a related + template was changed - ``m2m_templates_changed``: the assigned templates + were changed (either templates were added, removed or their order was + changed) -This signal is emitted every time the configuration of a device is modified. +This signal is emitted every time the configuration of a device is +modified. -It does not matter if ``Config.status`` is already modified, this signal will be emitted -anyway because it signals that the device configuration has changed. +It does not matter if ``Config.status`` is already modified, this signal +will be emitted anyway because it signals that the device configuration +has changed. -This signal is used to trigger the update of the configuration on devices, when the push -feature is enabled (requires Device credentials). +This signal is used to trigger the update of the configuration on devices, +when the push feature is enabled (requires Device credentials). -The signal is also emitted when one of the templates used by the device is modified or -if the templates assigned to the device are changed. +The signal is also emitted when one of the templates used by the device is +modified or if the templates assigned to the device are changed. Special cases in which ``config_modified`` is not emitted +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ This signal is not emitted when the device is created for the first time. -It is also not emitted when templates assigned to a config object are cleared -(``post_clear`` m2m signal), this is necessary because `sortedm2m -`_, the package we use to implement -ordered templates, uses the clear action to reorder templates (m2m relationships are -first cleared and then added back), therefore we ignore ``post_clear`` to avoid emitting -signals twice (one for the clear action and one for the add action). Please keep this in -mind if you plan on using the clear method of the m2m manager. +It is also not emitted when templates assigned to a config object are +cleared (``post_clear`` m2m signal), this is necessary because `sortedm2m +`_, the package we use to +implement ordered templates, uses the clear action to reorder templates +(m2m relationships are first cleared and then added back), therefore we +ignore ``post_clear`` to avoid emitting signals twice (one for the clear +action and one for the add action). Please keep this in mind if you plan +on using the clear method of the m2m manager. ``config_status_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -132,18 +138,21 @@ mind if you plan on using the clear method of the m2m manager. - ``instance``: instance of ``Config`` which got its ``status`` changed -This signal is emitted only when the configuration status of a device has changed. +This signal is emitted only when the configuration status of a device has +changed. -The signal is emitted also when the m2m template relationships of a config object are -changed, but only on ``post_add`` or ``post_remove`` actions, ``post_clear`` is ignored -for the same reason explained in the previous section. +The signal is emitted also when the m2m template relationships of a config +object are changed, but only on ``post_add`` or ``post_remove`` actions, +``post_clear`` is ignored for the same reason explained in the previous +section. .. _config_backend_changed: ``config_backend_changed`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_controller.config.signals.config_backend_changed`` **Arguments**: +**Path**: ``openwisp_controller.config.signals.config_backend_changed`` +**Arguments**: - ``instance``: instance of ``Config`` which got its ``backend`` changed - ``old_backend``: the old backend of the config object @@ -158,14 +167,15 @@ It is not emitted when the device or config is created. **Arguments**: -- ``instance``: instance of ``Device`` for which its configuration checksum has been - requested +- ``instance``: instance of ``Device`` for which its configuration + checksum has been requested - ``request``: the HTTP request object -This signal is emitted when a device requests a checksum via the controller views. +This signal is emitted when a device requests a checksum via the +controller views. -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. ``config_download_requested`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -174,15 +184,15 @@ the response was not successful. **Arguments**: -- ``instance``: instance of ``Device`` for which its configuration has been requested - for download +- ``instance``: instance of ``Device`` for which its configuration has + been requested for download - ``request``: the HTTP request object -This signal is emitted when a device requests to download its configuration via the -controller views. +This signal is emitted when a device requests to download its +configuration via the controller views. -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. ``is_working_changed`` ~~~~~~~~~~~~~~~~~~~~~~ @@ -193,11 +203,12 @@ the response was not successful. - ``instance``: instance of ``DeviceConnection`` - ``is_working``: value of ``DeviceConnection.is_working`` -- ``old_is_working``: previous value of ``DeviceConnection.is_working``, either ``None`` - (for new connections), ``True`` or ``False`` -- ``failure_reason``: error message explaining reason for failure in establishing - connection -- ``old_failure_reason``: previous value of ``DeviceConnection.failure_reason`` +- ``old_is_working``: previous value of ``DeviceConnection.is_working``, + either ``None`` (for new connections), ``True`` or ``False`` +- ``failure_reason``: error message explaining reason for failure in + establishing connection +- ``old_failure_reason``: previous value of + ``DeviceConnection.failure_reason`` This signal is emitted every time ``DeviceConnection.is_working`` changes. @@ -226,11 +237,12 @@ It is not triggered when the device is created for the first time. **Arguments**: - ``instance``: instance of ``Device`` which got registered. -- ``is_new``: boolean, will be ``True`` when the device is new, ``False`` when the - device already exists (eg: a device which gets a factory reset will register again) +- ``is_new``: boolean, will be ``True`` when the device is new, ``False`` + when the device already exists (eg: a device which gets a factory reset + will register again) -This signal is emitted when a device registers automatically through the controller HTTP -API. +This signal is emitted when a device registers automatically through the +controller HTTP API. ``device_name_changed`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -271,7 +283,8 @@ It is not emitted when the device is created. - ``instance``: instance of ``DeviceGroup``. - ``templates``: list of ``Template`` objects assigned to ``DeviceGroup`` -- ``old_templates``: list of ``Template`` objects assigned earlier to ``DeviceGroup`` +- ``old_templates``: list of ``Template`` objects assigned earlier to + ``DeviceGroup`` The signal is emitted when the device group templates changes. @@ -280,17 +293,18 @@ It is not emitted when the device is created. ``subnet_provisioned`` ~~~~~~~~~~~~~~~~~~~~~~ -**Path**: ``openwisp_controller.subnet_division.signals.subnet_provisioned`` +**Path**: +``openwisp_controller.subnet_division.signals.subnet_provisioned`` **Arguments**: - ``instance``: instance of ``VpnClient``. -- ``provisioned``: dictionary of ``Subnet`` and ``IpAddress`` provisioned, ``None`` if - nothing is provisioned +- ``provisioned``: dictionary of ``Subnet`` and ``IpAddress`` provisioned, + ``None`` if nothing is provisioned -The signal is emitted when subnets and IP addresses have been provisioned for a -``VpnClient`` for a VPN server with a subnet with :doc:`subnet division rule -<../user/subnet-division-rules>`. +The signal is emitted when subnets and IP addresses have been provisioned +for a ``VpnClient`` for a VPN server with a subnet with :doc:`subnet +division rule <../user/subnet-division-rules>`. ``vpn_server_modified`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -314,5 +328,5 @@ The signal is emitted when the VPN server is modified. The signal is emitted when the peers of VPN server gets changed. -It is only emitted for ``Vpn`` object with **WireGuard** or **VXLAN over WireGuard** -backend. +It is only emitted for ``Vpn`` object with **WireGuard** or **VXLAN over +WireGuard** backend. diff --git a/docs/index.rst b/docs/index.rst index 148869423..a2441654f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,9 +4,10 @@ Controller **Source code**: `github.com/openwisp/openwisp-controller `_. -OpenWISP Controller is responsible of of managing the core resources of the network and -allows automating several aspects like adoption, provisioning, VPN tunnel configuration, -generation of X509 certificates, subnet and IP address allocation and more. +OpenWISP Controller is responsible of of managing the core resources of +the network and allows automating several aspects like adoption, +provisioning, VPN tunnel configuration, generation of X509 certificates, +subnet and IP address allocation and more. For a full introduction please refer to :doc:`user/intro`. diff --git a/docs/partials/developer-docs.rst b/docs/partials/developer-docs.rst index f025d86e9..9cba48fbb 100644 --- a/docs/partials/developer-docs.rst +++ b/docs/partials/developer-docs.rst @@ -1,12 +1,12 @@ .. note:: - This documentation page is aimed at developers who want to customize, change or - extend the code of OpenWISP Controller 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). + This documentation page is aimed at developers who want to customize, + change or extend the code of OpenWISP Controller 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: + 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 Controller User Docs ` diff --git a/docs/partials/shared-object.rst b/docs/partials/shared-object.rst index 489ba9ebc..ca13c1805 100644 --- a/docs/partials/shared-object.rst +++ b/docs/partials/shared-object.rst @@ -1,8 +1,8 @@ .. note:: - This guide creates the VPN server and VPN client templates as **Shared systemwide - (no organization)** objects. This allows any device of any organization to use the - automation. + This guide creates the VPN server and VPN client templates as **Shared + systemwide (no organization)** objects. This allows any device of any + organization to use the automation. - If needed, you can use any organization as long as the VPN server, the VPN client - template, and devices have the same organization. + If needed, you can use any organization as long as the VPN server, the + VPN client template, and devices have the same organization. diff --git a/docs/user/device-groups.rst b/docs/user/device-groups.rst index 40f00578e..0bca0c3cd 100644 --- a/docs/user/device-groups.rst +++ b/docs/user/device-groups.rst @@ -1,10 +1,11 @@ Device Groups ============= -Device groups allow to group similar devices together, the groups usually share not only -a common characteristic but also some kind of organizational need: they need to have -specific configuration templates, variables and/or associated metadata which differs -from the rest of the network. +Device groups allow to group similar devices together, the groups usually +share not only a common characteristic but also some kind of +organizational need: they need to have specific configuration templates, +variables and/or associated metadata which differs from the rest of the +network. .. contents:: **Features provided by Device Groups:** :depth: 2 @@ -19,65 +20,72 @@ from the rest of the network. Group Templates --------------- -Groups allow to define templates which are automatically assigned to devices belonging -to the group. When using this feature, keep in mind the following important points: - -- Templates of any configuration backend can be selected, when a device is assigned to a - group, only the templates which matches the device configuration backend are applied - to the device. -- The system will not force group templates onto devices, this means that users can - remove the applied group templates from a specific device if needed. -- If a device group is changed, the system will automatically remove the group templates - of the old group and apply the new templates of the new group (this operation is - implemented by leveraging the :ref:`group_templates_changed` signal). -- If the group templates are changed, the devices which belong to the group will be - automatically updated to reflect the changes (this operation is executed in a - background task). -- In case the configuration backend of a device is changed, the system will handle this - automatically too and update the group templates accordingly (this operation is - implemented by leveraging the :ref:`config_backend_changed` signal). -- If a device does not have a configuration defined yet, but it is assigned to a group - which has templates defined, the system will automatically create a configuration for - it using the default backend specified in the - :ref:`OPENWISP_CONTROLLER_DEFAULT_BACKEND` setting. - -**Note:** the list of templates shown in the edit group page do not contain templates -flagged as :ref:`"default" ` or :ref:`"required" -` to avoid redundancy because those templates are automatically -assigned by the system to new devices. - -This feature works also when editing group templates or the group assigned to a device -via the :ref:`REST API `. +Groups allow to define templates which are automatically assigned to +devices belonging to the group. When using this feature, keep in mind the +following important points: + +- Templates of any configuration backend can be selected, when a device is + assigned to a group, only the templates which matches the device + configuration backend are applied to the device. +- The system will not force group templates onto devices, this means that + users can remove the applied group templates from a specific device if + needed. +- If a device group is changed, the system will automatically remove the + group templates of the old group and apply the new templates of the new + group (this operation is implemented by leveraging the + :ref:`group_templates_changed` signal). +- If the group templates are changed, the devices which belong to the + group will be automatically updated to reflect the changes (this + operation is executed in a background task). +- In case the configuration backend of a device is changed, the system + will handle this automatically too and update the group templates + accordingly (this operation is implemented by leveraging the + :ref:`config_backend_changed` signal). +- If a device does not have a configuration defined yet, but it is + assigned to a group which has templates defined, the system will + automatically create a configuration for it using the default backend + specified in the :ref:`OPENWISP_CONTROLLER_DEFAULT_BACKEND` setting. + +**Note:** the list of templates shown in the edit group page do not +contain templates flagged as :ref:`"default" ` or +:ref:`"required" ` to avoid redundancy because those +templates are automatically assigned by the system to new devices. + +This feature works also when editing group templates or the group assigned +to a device via the :ref:`REST API `. .. _device_group_variables: Group Configuration Variables ----------------------------- -Groups allow to define configuration variables which are automatically added to the -device's context in the **System Defined Variables**. Check the :doc:`./variables` -section to learn more about precedence of different configuration variables. +Groups allow to define configuration variables which are automatically +added to the device's context in the **System Defined Variables**. Check +the :doc:`./variables` section to learn more about precedence of different +configuration variables. -This feature also works when editing group templates or the group assigned to a device -via the :ref:`REST API `. +This feature also works when editing group templates or the group assigned +to a device via the :ref:`REST API `. Group Metadata -------------- -Groups allow to store additional information regarding a group in the structured -metadata field (which can be accessed via the REST API). +Groups allow to store additional information regarding a group in the +structured metadata field (which can be accessed via the REST API). -The metadata field allows custom structure and validation to standardize information -across all groups using the :ref:`OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA` setting. +The metadata field allows custom structure and validation to standardize +information across all groups using the +:ref:`OPENWISP_CONTROLLER_DEVICE_GROUP_SCHEMA` setting. Variables vs Metadata --------------------- -*Group configuration variables* and *Group metadata* serves different purposes. +*Group configuration variables* and *Group metadata* serves different +purposes. -The group configuration variables should be used when the device configuration is -required to be changed for particular group of devices. +The group configuration variables should be used when the device +configuration is required to be changed for particular group of devices. -Group metadata should be used to store additional data for the device group, this data -can be fetched and/or tweaked via the REST API if needed. Group metadata is not designed -to be used for configuration purposes. +Group metadata should be used to store additional data for the device +group, this data can be fetched and/or tweaked via the REST API if needed. +Group metadata is not designed to be used for configuration purposes. diff --git a/docs/user/import-export.rst b/docs/user/import-export.rst index f986a379c..c2d00f03a 100644 --- a/docs/user/import-export.rst +++ b/docs/user/import-export.rst @@ -5,16 +5,16 @@ Import/Export Device Data :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.1/import-export/device-list.png :alt: Import / Export -The device list page offers two buttons to export and import device data in different -formats. +The device list page offers two buttons to export and import device data +in different formats. Importing --------- -For importing devices into the system, only the required fields are needed, for example, -the following CSV file will import a device named ``TestImport`` with mac address -``00:11:22:09:44:55`` in the organization with UUID -``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``: +For importing devices into the system, only the required fields are +needed, for example, the following CSV file will import a device named +``TestImport`` with mac address ``00:11:22:09:44:55`` in the organization +with UUID ``3cb5e18c-0312-48ab-8dbd-038b8415bd6f``: .. code-block:: diff --git a/docs/user/intro.rst b/docs/user/intro.rst index 361758226..d9238f71f 100644 --- a/docs/user/intro.rst +++ b/docs/user/intro.rst @@ -10,30 +10,31 @@ OpenWISP Controller is a Python package which ships five Django apps. Config App ---------- -The config app is the core of the controller module and implements all the following -features: +The config app is the core of the controller module and implements all the +following features: - **Configuration management** for embedded devices supporting: - `OpenWrt `_ - - `OpenWISP Firmware `_ - - additional firmware can be added by :ref:`specifying custom configuration - backends ` + - `OpenWISP Firmware + `_ + - additional firmware can be added by :ref:`specifying custom + configuration backends ` - **Configuration editor** based on `JSON-Schema editor `_ -- **Advanced edit mode**: edit `NetJSON `_ *DeviceConfiguration* - objects for maximum flexibility -- :doc:`templates`: reduce repetition to the minimum, configure default and required - templates +- **Advanced edit mode**: edit `NetJSON `_ + *DeviceConfiguration* objects for maximum flexibility +- :doc:`templates`: reduce repetition to the minimum, configure default + and required templates - :doc:`variables`: reference variables in the configuration and templates -- :doc:`device-groups`: define different set of default configuration and metadata in - device groups -- :ref:`Template Tags `: define different sets of default templates (eg: - mesh, WDS, 4G) -- **HTTP resources**: allow devices to automatically check for and download - configuration updates -- **VPN management**: automatically provision VPN tunnel configurations, including - cryptographic keys and IP addresses, eg: :doc:`OpenVPN `, :doc:`WireGuard - ` +- :doc:`device-groups`: define different set of default configuration and + metadata in device groups +- :ref:`Template Tags `: define different sets of default + templates (eg: mesh, WDS, 4G) +- **HTTP resources**: allow devices to automatically check for and + download configuration updates +- **VPN management**: automatically provision VPN tunnel configurations, + including cryptographic keys and IP addresses, eg: :doc:`OpenVPN + `, :doc:`WireGuard ` - :doc:`import-export` It exposes various :doc:`REST API endpoints `. @@ -41,8 +42,9 @@ It exposes various :doc:`REST API endpoints `. PKI App ------- -The PKI app is based on `django-x509 `_, -allowing you to create, import, and view x509 CAs and certificates directly from the +The PKI app is based on `django-x509 +`_, allowing you to create, +import, and view x509 CAs and certificates directly from the administration dashboard. It exposes various :doc:`REST API endpoints `. @@ -50,25 +52,27 @@ It exposes various :doc:`REST API endpoints `. Connection App -------------- -This app enables OpenWISP Controller to use different protocols to reach network -devices. Currently, the default connection protocols are SSH and SNMP, but the protocol -mechanism is extensible, allowing for implementation of additional protocols if needed. +This app enables OpenWISP Controller to use different protocols to reach +network devices. Currently, the default connection protocols are SSH and +SNMP, but the protocol mechanism is extensible, allowing for +implementation of additional protocols if needed. It exposes various :doc:`REST API endpoints `. SSH ~~~ -The SSH connector allows the controller to initialize connections to the devices in -order to perform :doc:`push operations `, e.g.: +The SSH connector allows the controller to initialize connections to the +devices in order to perform :doc:`push operations `, +e.g.: - Sending configuration updates. - :doc:`Executing shell commands `. -- Perform firmware upgrades via the additional :doc:`firmware upgrade module - `. +- Perform firmware upgrades via the additional :doc:`firmware upgrade + module `. -The default connection protocol implemented is SSH, but other protocol mechanism is -extensible and custom protocols can be implemented as well. +The default connection protocol implemented is SSH, but other protocol +mechanism is extensible and custom protocols can be implemented as well. Access via SSH key is recommended, the SSH key algorithms supported are: @@ -78,18 +82,19 @@ Access via SSH key is recommended, the SSH key algorithms supported are: SNMP ~~~~ -The SNMP connector is useful to collect monitoring information and it's used in -:doc:`OpenWISP Monitoring ` for performing checks to collect -monitoring information. `Read more -`_ on -how to use it. +The SNMP connector is useful to collect monitoring information and it's +used in :doc:`OpenWISP Monitoring ` for performing +checks to collect monitoring information. `Read more +`_ +on how to use it. Geo App ------- -The geographic app is based on `django-loci `_ -and allows to define the geographic coordinates of the devices, as well as their indoor -coordinates on floorplan images. +The geographic app is based on `django-loci +`_ and allows to define the +geographic coordinates of the devices, as well as their indoor coordinates +on floorplan images. It exposes various :doc:`REST API endpoints `. @@ -101,12 +106,12 @@ Subnet Division App This app is optional, if you don't need it you can avoid adding it to ``settings.INSTALLED_APPS``. -This app allows to automatically provision subnets and IP addresses which will be -available as :ref:`system defined configuration variables ` -that can be used in :doc:`templates`. +This app allows to automatically provision subnets and IP addresses which +will be available as :ref:`system defined configuration variables +` that can be used in :doc:`templates`. -The purpose of this app is to allow users to automatically provision and configure -specific subnets and IP addresses to the devices without the need of manual -intervention. +The purpose of this app is to allow users to automatically provision and +configure specific subnets and IP addresses to the devices without the +need of manual intervention. Refer to :doc:`subnet-division-rules` for more information. diff --git a/docs/user/openvpn.rst b/docs/user/openvpn.rst index 245a307d7..d644ec807 100644 --- a/docs/user/openvpn.rst +++ b/docs/user/openvpn.rst @@ -210,8 +210,8 @@ Finally you can add the new template to your devices. .. tip:: If you need to troubleshoot any issue, increase the verbosity of the - OpenVPN logging, both on the server and the clients, and check both logs - (on the server and on the client). + OpenVPN logging, both on the server and the clients, and check both + logs (on the server and on the client). .. seealso:: diff --git a/docs/user/push-operations.rst b/docs/user/push-operations.rst index 0aa927418..54368b37a 100644 --- a/docs/user/push-operations.rst +++ b/docs/user/push-operations.rst @@ -11,36 +11,39 @@ Introduction .. important:: If you have installed OpenWISP with the `ansbile-openwisp2 role - `_ you can skip the following steps, - which are handled automatically by the ansible role during the first installation. + `_ you can skip the + following steps, which are handled automatically by the ansible role + during the first installation. -The Ansible role automatically creates a default template to update ``authorized_keys`` -on networking devices using the default access credentials. +The Ansible role automatically creates a default template to update +``authorized_keys`` on networking devices using the default access +credentials. -Follow the procedure described below to enable secure SSH access from OpenWISP to your -devices, this is required to enable push operations (whenever the configuration is -changed, OpenWISP will trigger the update in the background) and/or :doc:`firmware -upgrades (via the additional module openwisp-firmware-upgrader) -`. +Follow the procedure described below to enable secure SSH access from +OpenWISP to your devices, this is required to enable push operations +(whenever the configuration is changed, OpenWISP will trigger the update +in the background) and/or :doc:`firmware upgrades (via the additional +module openwisp-firmware-upgrader) `. 1. Generate SSH Key ------------------- -First of all, we need to generate the SSH key which will be used by OpenWISP to access -the devices, to do so, you can use the following command: +First of all, we need to generate the SSH key which will be used by +OpenWISP to access the devices, to do so, you can use the following +command: .. code-block:: shell echo './sshkey' | ssh-keygen -t ed25519 -C "openwisp" -This will create two files in the current directory, one called ``sshkey`` (the private -key) and one called ``sshkey.pub`` (the public key). +This will create two files in the current directory, one called ``sshkey`` +(the private key) and one called ``sshkey.pub`` (the public key). Store the content of these files in a secure location. -**Note:** Support for **ED25519** was added in OpenWrt 21.02 (requires Dropbear > -2020.79). If you are managing devices with OpenWrt < 21, then you will need to use RSA -keys: +**Note:** Support for **ED25519** was added in OpenWrt 21.02 (requires +Dropbear > 2020.79). If you are managing devices with OpenWrt < 21, then +you will need to use RSA keys: .. code-block:: shell @@ -53,19 +56,21 @@ keys: :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-ssh-credentials-private-key.png :alt: add SSH private key as access credential in OpenWISP -From the first page of OpenWISP click on "CONFIGURATIONS" in the left navigation menu, -then "Access credentials", then click on the **"ADD ACCESS CREDENTIALS"** button in the -upper right corner (alternatively, go to the following URL path: -``/admin/connection/credentials/add/``). +From the first page of OpenWISP click on "CONFIGURATIONS" in the left +navigation menu, then "Access credentials", then click on the **"ADD +ACCESS CREDENTIALS"** button in the upper right corner (alternatively, go +to the following URL path: ``/admin/connection/credentials/add/``). -Select SSH as ``type``, enable the **Auto add** checkbox, then at the field "Credentials -type" select "SSH (private key)", now type "root" in the ``username`` field, while in -the ``key`` field you have to paste the contents of the private key just created. +Select SSH as ``type``, enable the **Auto add** checkbox, then at the +field "Credentials type" select "SSH (private key)", now type "root" in +the ``username`` field, while in the ``key`` field you have to paste the +contents of the private key just created. Now hit save. -The credentials just created will be automatically enabled for all the devices in the -system (both existing devices and devices which will be added in the future). +The credentials just created will be automatically enabled for all the +devices in the system (both existing devices and devices which will be +added in the future). 3. Add the Public Key to Your Devices ------------------------------------- @@ -74,35 +79,39 @@ system (both existing devices and devices which will be added in the future). :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/add-authorized-ssh-keys-template.png :alt: Add authorized SSH public keys template to OpenWISP (OpenWrt) -Now we need to instruct your devices to allow OpenWISP accessing via SSH, in order to do -this we need to add the contents of the public key file created in step 1 -(``sshkey.pub``) in the file ``/etc/dropbear/authorized_keys`` on the devices, the -recommended way to do this is to create a configuration template in OpenWISP: from the -first page of OpenWISP, click on "CONFIGURATIONS" in the left navigation menu, then and -click on the **"ADD TEMPLATE"** button in the upper right corner (alternatively, go to -the following URL: ``/admin/config/template/add/``). - -Check **enabled by default**, then scroll down the configuration section, click on -"Configuration Menu", scroll down, click on "Files" then close the menu by clicking -again on "Configuration Menu". Now type ``/etc/dropbear/authorized_keys`` in the -``path`` field of the file, then paste the contents of ``sshkey.pub`` in ``contents``. +Now we need to instruct your devices to allow OpenWISP accessing via SSH, +in order to do this we need to add the contents of the public key file +created in step 1 (``sshkey.pub``) in the file +``/etc/dropbear/authorized_keys`` on the devices, the recommended way to +do this is to create a configuration template in OpenWISP: from the first +page of OpenWISP, click on "CONFIGURATIONS" in the left navigation menu, +then and click on the **"ADD TEMPLATE"** button in the upper right corner +(alternatively, go to the following URL: ``/admin/config/template/add/``). + +Check **enabled by default**, then scroll down the configuration section, +click on "Configuration Menu", scroll down, click on "Files" then close +the menu by clicking again on "Configuration Menu". Now type +``/etc/dropbear/authorized_keys`` in the ``path`` field of the file, then +paste the contents of ``sshkey.pub`` in ``contents``. Now hit save. -**There's a catch**: you will need to assign the template to any existing device. +**There's a catch**: you will need to assign the template to any existing +device. 4. Test It ---------- Once you have performed the 3 steps above, you can test it as follows: -1. Ensure there's at least one device turned on and connected to OpenWISP, ensure this - device has the "SSH Authorized Keys" assigned to it. -2. Ensure the celery worker of OpenWISP Controller is running (eg: ``ps aux | grep - celery``) +1. Ensure there's at least one device turned on and connected to OpenWISP, + ensure this device has the "SSH Authorized Keys" assigned to it. +2. Ensure the celery worker of OpenWISP Controller is running (eg: ``ps + aux | grep celery``) 3. SSH into the device and wait (maximum 2 minutes) until ``/etc/dropbear/authorized_keys`` appears as specified in the template. -4. While connected via SSH to the device run the following command in the console: - ``logread -f``, now try changing the device name in OpenWISP -5. Shortly after you change the name in OpenWISP, you should see some output in the SSH - console indicating another SSH access and the configuration update being performed. +4. While connected via SSH to the device run the following command in the + console: ``logread -f``, now try changing the device name in OpenWISP +5. Shortly after you change the name in OpenWISP, you should see some + output in the SSH console indicating another SSH access and the + configuration update being performed. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst index 3b7041c9e..b10dcdfbc 100644 --- a/docs/user/rest-api.rst +++ b/docs/user/rest-api.rst @@ -25,10 +25,10 @@ Browsable Web Interface :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/browsable-api-ui.png 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. +` 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. Authentication -------------- @@ -36,14 +36,14 @@ Authentication See :ref:`authenticating_rest_api`. When browsing the API via the :ref:`controller_live_documentation` or the -:ref:`controller_browsable_web_interface`, you can also use the session authentication -by logging in the django admin. +:ref:`controller_browsable_web_interface`, you can also use the session +authentication by logging in the django admin. Pagination ---------- -All *list* endpoints support the ``page_size`` parameter that allows paginating the -results in conjunction with the ``page`` parameter. +All *list* endpoints support the ``page_size`` parameter that allows +paginating the results in conjunction with the ``page`` parameter. .. code-block:: text @@ -55,10 +55,11 @@ results in conjunction with the ``page`` parameter. List of Endpoints ----------------- -Since the detailed explanation is contained in the :ref:`controller_live_documentation` -and in the :ref:`controller_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:`controller_live_documentation` and in the +:ref:`controller_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. List Devices ~~~~~~~~~~~~ @@ -69,15 +70,16 @@ List Devices **Available filters** -You can filter a list of devices based on their configuration status using the -``status`` (e.g modified, applied, or error). +You can filter a list of devices based on their configuration status using +the ``status`` (e.g modified, applied, or error). .. code-block:: text GET /api/v1/controller/device/?config__status={status} -You can filter a list of devices based on their configuration backend using the -``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). +You can filter a list of devices based on their configuration backend +using the ``backend`` (e.g netjsonconfig.OpenWrt or +netjsonconfig.OpenWisp). .. code-block:: text @@ -94,21 +96,22 @@ You can filter a list of devices based on their organization using the GET /api/v1/controller/device/?organization_slug={organization_slug} -You can filter a list of devices based on their configuration templates using the -``template_id``. +You can filter a list of devices based on their configuration templates +using the ``template_id``. .. code-block:: text GET /api/v1/controller/device/?config__templates={template_id} -You can filter a list of devices based on their device group using the ``group_id``. +You can filter a list of devices based on their device group using the +``group_id``. .. code-block:: text GET /api/v1/controller/device/?group={group_id} -You can filter a list of devices that have a device location object using the -``with_geo`` (eg. true or false). +You can filter a list of devices that have a device location object using +the ``with_geo`` (eg. true or false). .. code-block:: text @@ -149,8 +152,8 @@ Download Device Configuration GET /api/v1/controller/device/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file containing the generated -configuration for that specific device. +The above endpoint triggers the download of a ``tar.gz`` file containing +the generated configuration for that specific device. Change Details of Device ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -166,16 +169,17 @@ Patch Details of Device PATCH /api/v1/controller/device/{id}/ -**Note**: To assign, unassign, and change the order of the assigned templates add, -remove, and change the order of the ``{id}`` of the templates under the ``config`` field -in the JSON response respectively. Moreover, you can also select and unselect templates -in the HTML Form of the Browsable API. +**Note**: To assign, unassign, and change the order of the assigned +templates add, remove, and change the order of the ``{id}`` of the +templates under the ``config`` field in the JSON response respectively. +Moreover, you can also select and unselect templates in the HTML Form of +the Browsable API. -The required template(s) from the organization(s) of the device will added automatically -to the ``config`` and cannot be removed. +The required template(s) from the organization(s) of the device will added +automatically to the ``config`` and cannot be removed. -**Example usage**: For assigning template(s) add the/their {id} to the config of a -device, +**Example usage**: For assigning template(s) add the/their {id} to the +config of a device, .. code-block:: shell @@ -189,8 +193,8 @@ device, } }' -**Example usage**: For removing assigned templates, simply remove the/their {id} from -the config of a device, +**Example usage**: For removing assigned templates, simply remove +the/their {id} from the config of a device, .. code-block:: shell @@ -204,8 +208,8 @@ the config of a device, } }' -**Example usage**: For reordering the templates simply change their order from the -config of a device, +**Example usage**: For reordering the templates simply change their order +from the config of a device, .. code-block:: shell @@ -345,8 +349,8 @@ List Device Groups **Available filters** -You can filter a list of device groups based on their organization using the -``organization_id`` or ``organization_slug``. +You can filter a list of device groups based on their organization using +the ``organization_id`` or ``organization_slug``. .. code-block:: text @@ -356,8 +360,8 @@ You can filter a list of device groups based on their organization using the GET /api/v1/controller/group/?organization_slug={organization_slug} -You can filter a list of device groups that have a device object using the ``empty`` -(eg. true or false). +You can filter a list of device groups that have a device object using the +``empty`` (eg. true or false). .. code-block:: text @@ -395,12 +399,13 @@ Get Device Group from Certificate Common Name GET /api/v1/controller/cert/{common_name}/group/ -This endpoint can be used to retrieve group information and metadata by the common name -of a certificate used in a VPN client tunnel, this endpoint is used in layer 2 tunneling -solutions for firewall/captive portals. +This endpoint can be used to retrieve group information and metadata by +the common name of a certificate used in a VPN client tunnel, this +endpoint is used in layer 2 tunneling solutions for firewall/captive +portals. -It is also possible to filter device group by providing organization slug of -certificate's organization as show in the example below: +It is also possible to filter device group by providing organization slug +of certificate's organization as show in the example below: .. code-block:: text @@ -422,8 +427,8 @@ Create Device Location PUT /api/v1/controller/device/{id}/location/ -You can create ``DeviceLocation`` object by using primary keys of existing ``Location`` -and ``FloorPlan`` objects as shown in the example below. +You can create ``DeviceLocation`` object by using primary keys of existing +``Location`` and ``FloorPlan`` objects as shown in the example below. .. code-block:: json @@ -433,9 +438,10 @@ and ``FloorPlan`` objects as shown in the example below. "indoor": "-36,264" } -**Note:** The ``indoor`` field represents the coordinates of the point placed on the -image from the top left corner. E.g. if you placed the pointer on the top left corner of -the floorplan image, its indoor coordinates will be ``0,0``. +**Note:** The ``indoor`` field represents the coordinates of the point +placed on the image from the top left corner. E.g. if you placed the +pointer on the top left corner of the floorplan image, its indoor +coordinates will be ``0,0``. .. code-block:: text @@ -449,10 +455,11 @@ the floorplan image, its indoor coordinates will be ``0,0``. "indoor": "-36,264" }' -You can also create related ``Location`` and ``FloorPlan`` objects for the device -directly from this endpoint. +You can also create related ``Location`` and ``FloorPlan`` objects for the +device directly from this endpoint. -The following example demonstrates creating related location object in a single request. +The following example demonstrates creating related location object in a +single request. .. code-block:: json @@ -486,8 +493,8 @@ The following example demonstrates creating related location object in a single } }' -**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** format, -like following: +**Note:** You can also specify the ``geometry`` in **Well-known text +(WKT)** format, like following: .. code-block:: json @@ -500,10 +507,10 @@ like following: } } -Similarly, you can create ``Floorplan`` object with the same request. But, note that a -``FloorPlan`` can be added to ``DeviceLocation`` only if the related ``Location`` object -defines an indoor location. The example below demonstrates creating both ``Location`` -and ``FloorPlan`` objects. +Similarly, you can create ``Floorplan`` object with the same request. But, +note that a ``FloorPlan`` can be added to ``DeviceLocation`` only if the +related ``Location`` object defines an indoor location. The example below +demonstrates creating both ``Location`` and ``FloorPlan`` objects. .. code-block:: text @@ -533,11 +540,11 @@ and ``FloorPlan`` objects. -F floorplan.floor=1 \ -F 'floorplan.image=@floorplan.png' -**Note:** The request in above example uses ``multipart content-type`` for uploading -floorplan image. +**Note:** The request in above example uses ``multipart content-type`` for +uploading floorplan image. -You can also use an existing ``Location`` object and create a new floorplan for that -location using this endpoint. +You can also use an existing ``Location`` object and create a new +floorplan for that location using this endpoint. .. code-block:: text @@ -566,9 +573,10 @@ Change Details of Device Location PUT /api/v1/controller/device/{id}/location/ -**Note:** This endpoint can be used to update related ``Location`` and ``Floorplan`` -objects. Refer to the :ref:`examples in the "Create device location" section -` for information on payload format. +**Note:** This endpoint can be used to update related ``Location`` and +``Floorplan`` objects. Refer to the :ref:`examples in the "Create device +location" section ` for information on payload +format. Delete Device Location ~~~~~~~~~~~~~~~~~~~~~~ @@ -586,8 +594,9 @@ Get Device Coordinates **Note:** This endpoint is intended to be used by devices. -This endpoint skips multi-tenancy and permission checks if the device ``key`` is passed -as ``query_param`` because the system assumes that the device is updating it's position. +This endpoint skips multi-tenancy and permission checks if the device +``key`` is passed as ``query_param`` because the system assumes that the +device is updating it's position. .. code-block:: text @@ -603,8 +612,9 @@ Update Device Coordinates **Note:** This endpoint is intended to be used by devices. -This endpoint skips multi-tenancy and permission checks if the device ``key`` is passed -as ``query_param`` because the system assumes that the device is updating it's position. +This endpoint skips multi-tenancy and permission checks if the device +``key`` is passed as ``query_param`` because the system assumes that the +device is updating it's position. .. code-block:: json @@ -638,8 +648,8 @@ List Locations **Available filters** -You can filter using ``organization_id`` or ``organization_slug`` to get list locations -that belongs to an organization. +You can filter using ``organization_id`` or ``organization_slug`` to get +list locations that belongs to an organization. .. code-block:: text @@ -656,11 +666,11 @@ Create Location POST /api/v1/controller/location/ -If you are creating an ``indoor`` location, you can use this endpoint to create -floorplan for the location. +If you are creating an ``indoor`` location, you can use this endpoint to +create floorplan for the location. -The following example demonstrates creating floorplan along with location in a single -request. +The following example demonstrates creating floorplan along with location +in a single request. .. code-block:: text @@ -692,8 +702,8 @@ request. -F 'floorplan.image=@floorplan.png' \ -F organization=1f6c5666-1011-4f1d-bce9-fc6fcb4f3a05 -**Note:** You can also specify the ``geometry`` in **Well-known text (WKT)** format, -like following: +**Note:** You can also specify the ``geometry`` in **Well-known text +(WKT)** format, like following: .. code-block:: text @@ -722,8 +732,9 @@ Change Location Details PUT /api/v1/controller/location/{pk}/ -**Note**: Only the first floorplan data present can be edited or changed. Setting the -``type`` of location to outdoor will remove all the floorplans associated with it. +**Note**: Only the first floorplan data present can be edited or changed. +Setting the ``type`` of location to outdoor will remove all the floorplans +associated with it. Refer to the :ref:`examples in the "Create device location" section ` for information on payload format. @@ -745,7 +756,8 @@ List Devices in a Location List Locations with Devices Deployed (in GeoJSON Format) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Note**: this endpoint will only list locations that have been assigned to a device. +**Note**: this endpoint will only list locations that have been assigned +to a device. .. code-block:: text @@ -753,8 +765,8 @@ List Locations with Devices Deployed (in GeoJSON Format) **Available filters** -You can filter using ``organization_id`` or ``organization_slug`` to get list location -of devices from that organization. +You can filter using ``organization_id`` or ``organization_slug`` to get +list location of devices from that organization. .. code-block:: text @@ -773,8 +785,8 @@ List Floorplans **Available filters** -You can filter using ``organization_id`` or ``organization_slug`` to get list floorplans -that belongs to an organization. +You can filter using ``organization_id`` or ``organization_slug`` to get +list floorplans that belongs to an organization. .. code-block:: text @@ -832,29 +844,29 @@ You can filter a list of templates based on their organization using the GET /api/v1/controller/template/?organization_slug={organization_slug} -You can filter a list of templates based on their backend using the ``backend`` (e.g -netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). +You can filter a list of templates based on their backend using the +``backend`` (e.g netjsonconfig.OpenWrt or netjsonconfig.OpenWisp). .. code-block:: text GET /api/v1/controller/template/?backend={backend} -You can filter a list of templates based on their type using the ``type`` (eg. vpn or -generic). +You can filter a list of templates based on their type using the ``type`` +(eg. vpn or generic). .. code-block:: text GET /api/v1/controller/template/?type={type} -You can filter a list of templates that are enabled by default or not using the -``default`` (eg. true or false). +You can filter a list of templates that are enabled by default or not +using the ``default`` (eg. true or false). .. code-block:: text GET /api/v1/controller/template/?default={default} -You can filter a list of templates that are required or not using the ``required`` (eg. -true or false). +You can filter a list of templates that are required or not using the +``required`` (eg. true or false). .. code-block:: text @@ -898,8 +910,8 @@ Download Template Configuration GET /api/v1/controller/template/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file containing the generated -configuration for that specific template. +The above endpoint triggers the download of a ``tar.gz`` file containing +the generated configuration for that specific template. Change Details of Template ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -931,21 +943,23 @@ List VPNs **Available filters** -You can filter a list of vpns based on their backend using the ``backend`` (e.g -openwisp_controller.vpn_backends.OpenVpn or openwisp_controller.vpn_backends.Wireguard). +You can filter a list of vpns based on their backend using the ``backend`` +(e.g openwisp_controller.vpn_backends.OpenVpn or +openwisp_controller.vpn_backends.Wireguard). .. code-block:: text GET /api/v1/controller/vpn/?backend={backend} -You can filter a list of vpns based on their subnet using the ``subnet_id``. +You can filter a list of vpns based on their subnet using the +``subnet_id``. .. code-block:: text GET /api/v1/controller/vpn/?subnet={subnet_id} -You can filter a list of vpns based on their organization using the ``organization_id`` -or ``organization_slug``. +You can filter a list of vpns based on their organization using the +``organization_id`` or ``organization_slug``. .. code-block:: text @@ -976,8 +990,8 @@ Download VPN Configuration GET /api/v1/controller/vpn/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file containing the generated -configuration for that specific VPN. +The above endpoint triggers the download of a ``tar.gz`` file containing +the generated configuration for that specific VPN. Change Details of VPN ~~~~~~~~~~~~~~~~~~~~~ @@ -1021,8 +1035,9 @@ Import Existing CA POST /api/v1/controller/ca/ -**Note**: To import an existing CA, only ``name``, ``certificate`` and ``private_key`` -fields have to be filled in the ``HTML`` form or included in the ``JSON`` format. +**Note**: To import an existing CA, only ``name``, ``certificate`` and +``private_key`` fields have to be filled in the ``HTML`` form or included +in the ``JSON`` format. Get CA Detail ~~~~~~~~~~~~~ @@ -1052,8 +1067,8 @@ Download CA(crl) GET /api/v1/controller/ca/{id}/crl/ -The above endpoint triggers the download of ``{id}.crl`` file containing up to date CRL -of that specific CA. +The above endpoint triggers the download of ``{id}.crl`` file containing +up to date CRL of that specific CA. Delete CA ~~~~~~~~~ @@ -1090,9 +1105,9 @@ Import Existing Cert POST /api/v1/controller/cert/ -**Note**: To import an existing Cert, only ``name``, ``ca``, ``certificate`` and -``private_key`` fields have to be filled in the ``HTML`` form or included in the -``JSON`` format. +**Note**: To import an existing Cert, only ``name``, ``ca``, +``certificate`` and ``private_key`` fields have to be filled in the +``HTML`` form or included in the ``JSON`` format. Get Cert Detail ~~~~~~~~~~~~~~~ diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 8c1c7e2fe..47698239e 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -12,8 +12,8 @@ Settings **unit**: ``seconds`` ============ =========== -Configure timeout to wait for an authentication response when establishing a SSH -connection. +Configure timeout to wait for an authentication response when establishing +a SSH connection. ``OPENWISP_SSH_BANNER_TIMEOUT`` ------------------------------- @@ -24,8 +24,8 @@ connection. **unit**: ``seconds`` ============ =========== -Configure timeout to wait for the banner to be presented when establishing a SSH -connection. +Configure timeout to wait for the banner to be presented when establishing +a SSH connection. ``OPENWISP_SSH_COMMAND_TIMEOUT`` -------------------------------- @@ -36,8 +36,8 @@ connection. **unit**: ``seconds`` ============ =========== -Configure timeout on blocking read/write operations when executing a command in a SSH -connection. +Configure timeout on blocking read/write operations when executing a +command in a SSH connection. ``OPENWISP_SSH_CONNECTION_TIMEOUT`` ----------------------------------- @@ -70,28 +70,33 @@ Configure timeout for the TCP connect when establishing a SSH connection. ) ============ ================================================================================= -Available connector classes. Connectors are python classes that specify ways in which -OpenWISP can connect to devices in order to launch commands. +Available connector classes. Connectors are python classes that specify +ways in which OpenWISP can connect to devices in order to launch commands. ``OPENWISP_UPDATE_STRATEGIES`` ------------------------------ -============ ======================================================================================= +============ ============================================================================ **type**: ``tuple`` **default**: .. code-block:: python - (("openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt", "OpenWRT SSH"),) -============ ======================================================================================= + ( + ( + "openwisp_controller.connection.connectors.openwrt.ssh.OpenWrt", + "OpenWRT SSH", + ), + ) +============ ============================================================================ -Available update strategies. An update strategy is a subclass of a connector class which -defines an ``update_config`` method which is in charge of updating the configuration of -the device. +Available update strategies. An update strategy is a subclass of a +connector class which defines an ``update_config`` method which is in +charge of updating the configuration of the device. -This operation is launched in a background worker when the configuration of a device is -changed. +This operation is launched in a background worker when the configuration +of a device is changed. -It's possible to write custom update strategies and add them to this setting to make -them available in OpenWISP. +It's possible to write custom update strategies and add them to this +setting to make them available in OpenWISP. ``OPENWISP_CONFIG_UPDATE_MAPPING`` ---------------------------------- @@ -105,9 +110,9 @@ them available in OpenWISP. } ============ ================================================================== -A dictionary that maps configuration backends to update strategies in order to -automatically determine the update strategy of a device connection if the update -strategy field is left blank by the user. +A dictionary that maps configuration backends to update strategies in +order to automatically determine the update strategy of a device +connection if the update strategy field is left blank by the user. .. _openwisp_controller_backends: @@ -124,35 +129,41 @@ strategy field is left blank by the user. ) ============ =============================================== -Available configuration backends. For more information, see `netjsonconfig backends +Available configuration backends. For more information, see `netjsonconfig +backends `_. ``OPENWISP_CONTROLLER_VPN_BACKENDS`` ------------------------------------ -============ ==================================================================================== +============ ==================================================================== **type**: ``tuple`` **default**: .. code-block:: python ( ("openwisp_controller.vpn_backends.OpenVpn", "OpenVPN"), ("openwisp_controller.vpn_backends.Wireguard", "WireGuard"), - ("openwisp_controller.vpn_backends.VxlanWireguard", "VXLAN over WireGuard"), + ( + "openwisp_controller.vpn_backends.VxlanWireguard", + "VXLAN over WireGuard", + ), ("openwisp_controller.vpn_backends.ZeroTier", "ZeroTier"), ) -============ ==================================================================================== +============ ==================================================================== -Available VPN backends for VPN Server objects. For more information, see `netjsonconfig -VPN backends +Available VPN backends for VPN Server objects. For more information, see +`netjsonconfig VPN backends `_. A VPN backend must follow some basic rules in order to be compatible with *openwisp-controller*: - it MUST allow at minimum and at maximum one VPN instance -- the main *NetJSON* property MUST match the lowercase version of the class name, eg: - when using the ``OpenVpn`` backend, the system will look into ``config['openvpn']`` -- it SHOULD focus on the server capabilities of the VPN software being used +- the main *NetJSON* property MUST match the lowercase version of the + class name, eg: when using the ``OpenVpn`` backend, the system will look + into ``config['openvpn']`` +- it SHOULD focus on the server capabilities of the VPN software being + used .. _openwisp_controller_default_backend: @@ -164,11 +175,12 @@ A VPN backend must follow some basic rules in order to be compatible with **default**: ``OPENWISP_CONTROLLER_BACKENDS[0][0]`` ============ ====================================== -The preferred backend that will be used as initial value when adding new ``Config`` or -``Template`` objects in the admin. +The preferred backend that will be used as initial value when adding new +``Config`` or ``Template`` objects in the admin. This setting defaults to the raw value of the first item in the -``OPENWISP_CONTROLLER_BACKENDS`` setting, which is ``netjsonconfig.OpenWrt``. +``OPENWISP_CONTROLLER_BACKENDS`` setting, which is +``netjsonconfig.OpenWrt``. Setting it to ``None`` will force the user to choose explicitly. @@ -180,8 +192,8 @@ Setting it to ``None`` will force the user to choose explicitly. **default**: ``OPENWISP_CONTROLLER_VPN_BACKENDS[0][0]`` ============ ========================================== -The preferred backend that will be used as initial value when adding new ``Vpn`` objects -in the admin. +The preferred backend that will be used as initial value when adding new +``Vpn`` objects in the admin. This setting defaults to the raw value of the first item in the ``OPENWISP_CONTROLLER_VPN_BACKENDS`` setting, which is @@ -203,8 +215,7 @@ This feature is enabled by default. Auto-registration must be supported on the devices in order to work, see :doc:`openwisp-config automatic registration -` for more -information. +` for more information. .. _openwisp_controller_consistent_registration: @@ -216,15 +227,15 @@ information. **default**: ``True`` ============ ======== -Whether devices that are already registered are recognized when reflashed or reset, -hence keeping the existing configuration without creating a new one. +Whether devices that are already registered are recognized when reflashed +or reset, hence keeping the existing configuration without creating a new +one. This feature is enabled by default. -Auto-registration must be enabled also on the devices in order to work, see -:ref:`openwisp-config consistent key generation -` for more -information. +Auto-registration must be enabled also on the devices in order to work, +see :ref:`openwisp-config consistent key generation +` for more information. ``OPENWISP_CONTROLLER_REGISTRATION_SELF_CREATION`` -------------------------------------------------- @@ -234,12 +245,13 @@ information. **default**: ``True`` ============ ======== -Whether devices that are not already present in the system are allowed to register or -not. +Whether devices that are not already present in the system are allowed to +register or not. -Turn this off if you still want to use auto-registration to avoid having to manually set -the device UUID and key in its configuration file but also want to avoid indiscriminate -registration of new devices without explicit permission. +Turn this off if you still want to use auto-registration to avoid having +to manually set the device UUID and key in its configuration file but also +want to avoid indiscriminate registration of new devices without explicit +permission. .. _context_setting: @@ -251,16 +263,17 @@ registration of new devices without explicit permission. **default**: ``{}`` ============ ======== -Additional context that is passed to the default context of each device object. +Additional context that is passed to the default context of each device +object. -``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide configuration -variables. +``OPENWISP_CONTROLLER_CONTEXT`` can be used to define system-wide +configuration variables. -For more information regarding how to use configuration variables in OpenWISP, refer to -:doc:`variables`. +For more information regarding how to use configuration variables in +OpenWISP, refer to :doc:`variables`. -For technical information about how variables are handled in the lower levels of -OpenWISP, see `netjsonconfig context: configuration variables +For technical information about how variables are handled in the lower +levels of OpenWISP, see `netjsonconfig context: configuration variables `_. ``OPENWISP_CONTROLLER_DEFAULT_AUTO_CERT`` @@ -273,24 +286,27 @@ OpenWISP, see `netjsonconfig context: configuration variables The default value of the ``auto_cert`` field for new ``Template`` objects. -The ``auto_cert`` field is valid only for templates which have ``type`` set to ``VPN`` -and indicates whether configuration regarding the VPN tunnel is provisioned -automatically to each device using the template, eg: +The ``auto_cert`` field is valid only for templates which have ``type`` +set to ``VPN`` and indicates whether configuration regarding the VPN +tunnel is provisioned automatically to each device using the template, eg: -- when using OpenVPN, new `x509 `_ certificates - will be generated automatically using the same CA assigned to the related VPN object -- when using WireGuard, new pair of private and public keys (using `Curve25519 - `_) will be generated, as well as an IP address of the - subnet assigned to the related VPN object -- when using `VXLAN `_ tunnels over Wireguad, in - addition to the configuration generated for WireGuard, a new VID will be generated - automatically for each device if the configuration option "auto VNI" is turned on in - the VPN object +- when using OpenVPN, new `x509 `_ + certificates will be generated automatically using the same CA assigned + to the related VPN object +- when using WireGuard, new pair of private and public keys (using + `Curve25519 `_) will be generated, as well as + an IP address of the subnet assigned to the related VPN object +- when using `VXLAN `_ tunnels over + Wireguad, in addition to the configuration generated for WireGuard, a + new VID will be generated automatically for each device if the + configuration option "auto VNI" is turned on in the VPN object -All these auto generated configuration options will be available as template variables. +All these auto generated configuration options will be available as +template variables. -The objects that are automatically created will also be removed when they are not needed -anymore (eg: when the VPN template is removed from a configuration object). +The objects that are automatically created will also be removed when they +are not needed anymore (eg: when the VPN template is removed from a +configuration object). ``OPENWISP_CONTROLLER_CERT_PATH`` --------------------------------- @@ -300,8 +316,9 @@ anymore (eg: when the VPN template is removed from a configuration object). **default**: ``/etc/x509`` ============ ============= -The filesystem path where x509 certificate will be installed when downloaded on routers -when ``auto_cert`` is being used (enabled by default). +The filesystem path where x509 certificate will be installed when +downloaded on routers when ``auto_cert`` is being used (enabled by +default). ``OPENWISP_CONTROLLER_COMMON_NAME_FORMAT`` ------------------------------------------ @@ -311,14 +328,17 @@ when ``auto_cert`` is being used (enabled by default). **default**: ``{mac_address}-{name}`` ============ ======================== -Defines the format of the ``common_name`` attribute of VPN client certificates that are -automatically created when using VPN templates which have ``auto_cert`` set to ``True``. -A unique slug generated using `shortuuid `_ -is appended to the common name to introduce uniqueness. Therefore, resulting common -names will have ``{OPENWISP_CONTROLLER_COMMON_NAME_FORMAT}-{unique-slug}`` format. +Defines the format of the ``common_name`` attribute of VPN client +certificates that are automatically created when using VPN templates which +have ``auto_cert`` set to ``True``. A unique slug generated using +`shortuuid `_ is appended to +the common name to introduce uniqueness. Therefore, resulting common names +will have ``{OPENWISP_CONTROLLER_COMMON_NAME_FORMAT}-{unique-slug}`` +format. -**Note:** If the ``name`` and ``mac address`` of the device are equal, the ``name`` of -the device will be omitted from the common name to avoid redundancy. +**Note:** If the ``name`` and ``mac address`` of the device are equal, the +``name`` of the device will be omitted from the common name to avoid +redundancy. ``OPENWISP_CONTROLLER_MANAGEMENT_IP_DEVICE_LIST`` ------------------------------------------------- @@ -328,14 +348,15 @@ the device will be omitted from the common name to avoid redundancy. **default**: ``True`` ============ ======== -In the device list page, the column ``IP`` will show the ``management_ip`` if available, -defaulting to ``last_ip`` otherwise. +In the device list page, the column ``IP`` will show the ``management_ip`` +if available, defaulting to ``last_ip`` otherwise. -If this setting is set to ``False`` the ``management_ip`` won't be shown in the device -list page even if present, it will be shown only in the device detail page. +If this setting is set to ``False`` the ``management_ip`` won't be shown +in the device list page even if present, it will be shown only in the +device detail page. -You may set this to ``False`` if for some reason the majority of your user doesn't care -about the management ip address. +You may set this to ``False`` if for some reason the majority of your user +doesn't care about the management ip address. ``OPENWISP_CONTROLLER_CONFIG_BACKEND_FIELD_SHOWN`` -------------------------------------------------- @@ -345,12 +366,15 @@ about the management ip address. **default**: ``True`` ============ ======== -This setting toggles the ``backend`` fields in add/edit pages in Device and Template -configuration, as well as the ``backend`` field/filter in Device list and Template list. +This setting toggles the ``backend`` fields in add/edit pages in Device +and Template configuration, as well as the ``backend`` field/filter in +Device list and Template list. -If this setting is set to ``False`` these items will be removed from the UI. +If this setting is set to ``False`` these items will be removed from the +UI. -Note: This setting affects only the configuration backend and NOT the VPN backend. +Note: This setting affects only the configuration backend and NOT the VPN +backend. ``OPENWISP_CONTROLLER_DEVICE_NAME_UNIQUE`` ------------------------------------------ @@ -360,11 +384,11 @@ Note: This setting affects only the configuration backend and NOT the VPN backen **default**: ``True`` ============ ======== -This setting conditionally enforces unique Device names in an Organization. The query to -enforce this is case-insensitive. +This setting conditionally enforces unique Device names in an +Organization. The query to enforce this is case-insensitive. -Note: For this constraint to be optional, it is enforced on an application level and not -on database. +Note: For this constraint to be optional, it is enforced on an application +level and not on database. .. _openwisp_controller_hardware_id_enabled: @@ -376,11 +400,11 @@ on database. **default**: ``False`` ============ ========= -The field ``hardware_id`` can be used to store a unique hardware id, for example a -serial number. +The field ``hardware_id`` can be used to store a unique hardware id, for +example a serial number. -If this setting is set to ``True`` then this field will be shown first in the device -list page and in the add/edit device page. +If this setting is set to ``True`` then this field will be shown first in +the device list page and in the add/edit device page. This feature is disabled by default. @@ -404,7 +428,8 @@ This feature is disabled by default. Options for the model field ``hardware_id``. - ``blank``: wether the field is allowed to be blank -- ``null``: wether an empty value will be stored as ``NULL`` in the database +- ``null``: wether an empty value will be stored as ``NULL`` in the + database - ``max_length``: maximum length of the field - ``unique``: wether the value of the field must be unique - ``verbose_name``: text for the human readable label of the field @@ -418,10 +443,11 @@ Options for the model field ``hardware_id``. **default**: ``True`` ============ ======== -When the hardware ID feature is enabled, devices will be referenced with their hardware -ID instead of their name. +When the hardware ID feature is enabled, devices will be referenced with +their hardware ID instead of their name. -If you still want to reference devices by their name, set this to ``False``. +If you still want to reference devices by their name, set this to +``False``. ``OPENWISP_CONTROLLER_DEVICE_VERBOSE_NAME`` ------------------------------------------- @@ -431,11 +457,12 @@ If you still want to reference devices by their name, set this to ``False``. **default**: ``('Device', 'Devices')`` ============ ========================= -Defines the ``verbose_name`` attribute of the ``Device`` model, which is displayed in -the admin site. The first and second element of the tuple represent the singular and -plural forms. +Defines the ``verbose_name`` attribute of the ``Device`` model, which is +displayed in the admin site. The first and second element of the tuple +represent the singular and plural forms. -For example, if we want to change the verbose name to "Hotspot", we could write: +For example, if we want to change the verbose name to "Hotspot", we could +write: .. code-block:: python @@ -449,9 +476,9 @@ For example, if we want to change the verbose name to "Hotspot", we could write: **default**: ``False`` ============ ========= -Setting this to ``True`` will hide subnets and IP addresses generated by :doc:`subnet -division rules ` from being displayed in the list of Subnets and -IP addresses in the admin dashboard. +Setting this to ``True`` will hide subnets and IP addresses generated by +:doc:`subnet division rules ` from being displayed +in the list of Subnets and IP addresses in the admin dashboard. .. _openwisp_controller_subnet_division_types: @@ -474,7 +501,8 @@ IP addresses in the admin dashboard. ) ============ ================================================================================================= -Available types for :doc:`Subject Division Rule ` objects. +Available types for :doc:`Subject Division Rule ` +objects. For more information on how to write your own types, please refer to: :ref:`custom_subnet_division_rule_types`. @@ -487,8 +515,9 @@ For more information on how to write your own types, please refer to: **default**: ``True`` ============ ======== -Indicates whether the API for Openwisp Controller is enabled or not. To disable the API -by default add ``OPENWISP_CONTROLLER_API = False`` in your project ``settings.py`` file. +Indicates whether the API for Openwisp Controller is enabled or not. To +disable the API by default add ``OPENWISP_CONTROLLER_API = False`` in your +project ``settings.py`` file. ``OPENWISP_CONTROLLER_API_HOST`` -------------------------------- @@ -498,7 +527,8 @@ by default add ``OPENWISP_CONTROLLER_API = False`` in your project ``settings.py **default**: ``None`` ============ ======== -Allows to specify backend URL for API requests, if the frontend is hosted separately. +Allows to specify backend URL for API requests, if the frontend is hosted +separately. .. _openwisp_controller_user_commands: @@ -510,8 +540,8 @@ Allows to specify backend URL for API requests, if the frontend is hosted separa **default**: ``[]`` ============ ======== -Allows to specify a ``list`` of tuples for adding commands as described in the section: -:ref:`defining_new_menu_options`. +Allows to specify a ``list`` of tuples for adding commands as described in +the section: :ref:`defining_new_menu_options`. ``OPENWISP_CONTROLLER_ORGANIZATION_ENABLED_COMMANDS`` ----------------------------------------------------- @@ -526,9 +556,10 @@ Allows to specify a ``list`` of tuples for adding commands as described in the s } ============ ============================================= -This setting controls the command types that are enabled on the system By default, all -command types are enabled to all the organizations, but it's possible to disable a -specific command for a specific organization as shown in the following example: +This setting controls the command types that are enabled on the system By +default, all command types are enabled to all the organizations, but it's +possible to disable a specific command for a specific organization as +shown in the following example: .. code-block:: python @@ -539,9 +570,9 @@ specific command for a specific organization as shown in the following example: } In the example above, the organization with UUID -``7448a190-6e65-42bf-b8ea-bb6603e593a5`` will allow to send only commands of type -``reboot`` and ``change_password``, while all the other organizations will have all -command types enabled. +``7448a190-6e65-42bf-b8ea-bb6603e593a5`` will allow to send only commands +of type ``reboot`` and ``change_password``, while all the other +organizations will have all command types enabled. .. _openwisp_controller_device_group_schema: @@ -553,7 +584,8 @@ command types enabled. **default**: ``{'type': 'object', 'properties': {}}`` ============ ======================================== -Allows specifying JSONSchema used for validating the meta-data of :doc:`device-groups`. +Allows specifying JSONSchema used for validating the meta-data of +:doc:`device-groups`. ``OPENWISP_CONTROLLER_SHARED_MANAGEMENT_IP_ADDRESS_SPACE`` ---------------------------------------------------------- @@ -563,18 +595,20 @@ Allows specifying JSONSchema used for validating the meta-data of :doc:`device-g **default**: ``True`` ============ ======== -By default, the system assumes that the address space of the management tunnel is shared -among all the organizations using the system, that is, the system assumes there's only -one management VPN, tunnel or other networking technology to reach the devices it -controls. +By default, the system assumes that the address space of the management +tunnel is shared among all the organizations using the system, that is, +the system assumes there's only one management VPN, tunnel or other +networking technology to reach the devices it controls. -When set to ``True``, any device belonging to any organization will never have the same -``management_ip`` as another device, the latest device declaring the management IP will -take the IP and any other device who declared the same IP in the past will have the -field reset to empty state to avoid potential conflicts. +When set to ``True``, any device belonging to any organization will never +have the same ``management_ip`` as another device, the latest device +declaring the management IP will take the IP and any other device who +declared the same IP in the past will have the field reset to empty state +to avoid potential conflicts. -Set this to ``False`` if every organization has its dedicated management tunnel with a -dedicated address space that is reachable by the OpenWISP server. +Set this to ``False`` if every organization has its dedicated management +tunnel with a dedicated address space that is reachable by the OpenWISP +server. .. _openwisp_controller_management_ip_only: @@ -586,12 +620,12 @@ dedicated address space that is reachable by the OpenWISP server. **default**: ``True`` ============ ======== -By default, only the management IP will be used to establish connection with the -devices. +By default, only the management IP will be used to establish connection +with 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``. ``OPENWISP_CONTROLLER_DSA_OS_MAPPING`` -------------------------------------- @@ -601,20 +635,21 @@ this to ``False``. **default**: ``{}`` ============ ======== -OpenWISP Controller can figure out whether it should use the new OpenWrt syntax for DSA -interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by reading the -``os`` field of the ``Device`` object. However, if the firmware you are using has a -custom firmware identifier, the system will not be able to figure out whether it should -use the new syntax and it will default to :ref:`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK +OpenWISP Controller can figure out whether it should use the new OpenWrt +syntax for DSA interfaces (Distributed Switch Architecture) introduced in +OpenWrt 21 by reading the ``os`` field of the ``Device`` object. However, +if the firmware you are using has a custom firmware identifier, the system +will not be able to figure out whether it should use the new syntax and it +will default to :ref:`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK `. -If you want to make sure the system can parse your custom firmware identifier properly, -you can follow the example below. +If you want to make sure the system can parse your custom firmware +identifier properly, you can follow the example below. -For the sake of the example, the OS identifier ``MyCustomFirmware 2.0`` corresponds to -``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` corresponds to ``OpenWrt 21.02``. -Configuring this setting as indicated below will allow OpenWISP to supply the right -syntax automatically. +For the sake of the example, the OS identifier ``MyCustomFirmware 2.0`` +corresponds to ``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` +corresponds to ``OpenWrt 21.02``. Configuring this setting as indicated +below will allow OpenWISP to supply the right syntax automatically. Example: @@ -631,7 +666,8 @@ Example: } } -**Note**: The OS identifier should be a regular expression as shown in above example. +**Note**: The OS identifier should be a regular expression as shown in +above example. .. _openwisp_controller_dsa_default_fallback: @@ -643,8 +679,9 @@ Example: **default**: ``True`` ============ ======== -The value of this setting decides whether to use DSA syntax (OpenWrt >=21 configuration -syntax) if openwisp-controller fails to make that decision automatically. +The value of this setting decides whether to use DSA syntax (OpenWrt >=21 +configuration syntax) if openwisp-controller fails to make that decision +automatically. ``OPENWISP_CONTROLLER_GROUP_PIE_CHART`` --------------------------------------- @@ -659,8 +696,8 @@ Allows to show a pie chart like the one in the screenshot. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/devicegroups-piechart.png :alt: device groups piechart -Active groups are groups which have at least one device in them, while emtpy groups do -not have any device assigned. +Active groups are groups which have at least one device in them, while +emtpy groups do not have any device assigned. ``OPENWISP_CONTROLLER_API_TASK_RETRY_OPTIONS`` ---------------------------------------------- @@ -681,16 +718,16 @@ not have any device assigned. retry_jitter=True, # randomness into exponential backoff ) -This setting is utilized by background API tasks executed by :doc:`ZeroTier VPN servers -and ZeroTier VPN clients ` to handle recoverable HTTP status codes such as -429, 500, 502, 503, and 504. +This setting is utilized by background API tasks executed by +:doc:`ZeroTier VPN servers and ZeroTier VPN clients ` to handle +recoverable HTTP status codes such as 429, 500, 502, 503, and 504. -These tasks are retried with a maximum of 5 attempts with an exponential backoff and -jitter, with a maximum delay of 10 minutes. +These tasks are retried with a maximum of 5 attempts with an exponential +backoff and jitter, with a maximum delay of 10 minutes. -This feature ensures that ZeroTier Service API calls are resilient to recoverable -failures, improving the reliability of the system. +This feature ensures that ZeroTier Service API calls are resilient to +recoverable failures, improving the reliability of the system. -For more information on these settings, you can refer to the `the celery documentation -regarding automatic retries for known errors. +For more information on these settings, you can refer to the `the celery +documentation regarding automatic retries for known errors. `_ diff --git a/docs/user/shell-commands.rst b/docs/user/shell-commands.rst index 626fd064b..6d5165939 100644 --- a/docs/user/shell-commands.rst +++ b/docs/user/shell-commands.rst @@ -14,38 +14,42 @@ By default, there are three options in the **Send Command** dropdown: 2. Change Password 3. Custom Command -While the first two options are self-explanatory, the **custom command** option allows -you to execute any command on the device as shown in the example below. +While the first two options are self-explanatory, the **custom command** +option allows you to execute any command on the device as shown in the +example below. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/commands_demo.gif :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/commands_demo.gif :alt: Executing commands on device example -**Note**: in order for this feature to work, a device needs to have at least one -**Access Credential** (see :doc:`How to configure push updates `). +**Note**: in order for this feature to work, a device needs to have at +least one **Access Credential** (see :doc:`How to configure push updates +`). -The **Send Command** button will be hidden until the device has at least one **Access -Credential**. +The **Send Command** button will be hidden until the device has at least +one **Access Credential**. -If you need to allow your users to quickly send specific commands that are used often in -your network regardless of your users' knowledge of Linux shell commands, you can add -new commands by following instructions in the :ref:`defining_new_menu_options` section -below. +If you need to allow your users to quickly send specific commands that are +used often in your network regardless of your users' knowledge of Linux +shell commands, you can add new commands by following instructions in the +:ref:`defining_new_menu_options` section below. .. note:: If you're an advanced user and want to learn how to register commands - programmatically, refer to the :ref:`registering_unregistering_commands` section. + programmatically, refer to the + :ref:`registering_unregistering_commands` section. .. _defining_new_menu_options: Defining New Options in the Commands Menu ----------------------------------------- -Let's explore to define new custom commands to help users perform additional management -actions without having to be Linux/Unix experts. +Let's explore to define new custom commands to help users perform +additional management actions without having to be Linux/Unix experts. -We can do so by using the ``OPENWISP_CONTROLLER_USER_COMMANDS`` django setting. +We can do so by using the ``OPENWISP_CONTROLLER_USER_COMMANDS`` django +setting. The following example defines a simple command that can ``ping`` an input ``destination_address`` through a network interface, ``interface_name``. @@ -89,24 +93,25 @@ The following example defines a simple command that can ``ping`` an input ) ] -The above code will add the *Ping* command in the user interface as show in the GIF -below: +The above code will add the *Ping* command in the user interface as show +in the GIF below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/ping_command_example.gif :target: https://github.com/openwisp/openwisp-controller/tree/docs/docs/ping_command_example.gif :alt: Adding a *ping* command -The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of ``tuple`` each -containing two elements. The first element of the tuple should contain an identifier for -the command and the second element should contain a ``dict`` defining configuration of -the command. +The ``OPENWISP_CONTROLLER_USER_COMMANDS`` setting takes a ``list`` of +``tuple`` each containing two elements. The first element of the tuple +should contain an identifier for the command and the second element should +contain a ``dict`` defining configuration of the command. .. _comand_configuration: Command Configuration ~~~~~~~~~~~~~~~~~~~~~ -The ``dict`` defining configuration for command should contain following keys: +The ``dict`` defining configuration for command should contain following +keys: 1. ``label`` ++++++++++++ @@ -116,9 +121,9 @@ A ``str`` defining label for the command used internally by Django. 2. ``schema`` +++++++++++++ -A ``dict`` defining `JSONSchema `_ for inputs of command. You -can specify the inputs for your command, add rules for performing validation and make -inputs required or optional. +A ``dict`` defining `JSONSchema `_ for inputs of +command. You can specify the inputs for your command, add rules for +performing validation and make inputs required or optional. Here is a detailed explanation of the schema used in above example: @@ -151,19 +156,22 @@ Here is a detailed explanation of the schema used in above example: "additionalProperties": False, } -This example uses only handful of properties available in JSONSchema. You can experiment -with other properties of JSONSchema for schema of your command. +This example uses only handful of properties available in JSONSchema. You +can experiment with other properties of JSONSchema for schema of your +command. 3. ``callable`` +++++++++++++++ -A ``callable`` or ``str`` defining dotted path to a callable. It should return the -command (``str``) to be executed on the device. Inputs of the command are passed as -arguments to this callable. +A ``callable`` or ``str`` defining dotted path to a callable. It should +return the command (``str``) to be executed on the device. Inputs of the +command are passed as arguments to this callable. -The example above includes a callable(``ping_command_callable``) for ``ping`` command. +The example above includes a callable(``ping_command_callable``) for +``ping`` command. How to register or unregister commands -------------------------------------- -Refer to :ref:`registering_unregistering_commands` in the developer documentation. +Refer to :ref:`registering_unregistering_commands` in the developer +documentation. diff --git a/docs/user/subnet-division-rules.rst b/docs/user/subnet-division-rules.rst index 6e56d9692..d9e8deef6 100644 --- a/docs/user/subnet-division-rules.rst +++ b/docs/user/subnet-division-rules.rst @@ -1,8 +1,8 @@ Automating Subnet and IP Address Provisioning ============================================= -This guide helps you automate provisioning subnets and IP addresses for your network -devices. +This guide helps you automate provisioning subnets and IP addresses for +your network devices. .. contents:: **Table of Contents**: :depth: 2 @@ -15,8 +15,8 @@ devices. Create a master subnet. -This is the parent subnet from which automatically generated subnets will be -provisioned. +This is the parent subnet from which automatically generated subnets will +be provisioned. .. note:: @@ -26,43 +26,44 @@ provisioned. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet.png :alt: Creating a master subnet example -On the same page, add a **subnet division rule**. This rule defines the criteria for -automatically provisioning subnets under the master subnet. +On the same page, add a **subnet division rule**. This rule defines the +criteria for automatically provisioning subnets under the master subnet. -The type of subnet division rule determines when subnets and IP addresses are assigned -to devices. +The type of subnet division rule determines when subnets and IP addresses +are assigned to devices. The currently supported rule types are described below. .. note:: - For information on how to write your own subnet division rule types, please refer - to: :ref:`custom_subnet_division_rule_types`. + For information on how to write your own subnet division rule types, + please refer to: :ref:`custom_subnet_division_rule_types`. .. _device_rule: Device Subnet Division Rule ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This rule triggers when a device configuration (``config.Config`` model) is created for -the organization specified in the rule. +This rule triggers when a device configuration (``config.Config`` model) +is created for the organization specified in the rule. .. note:: - If a device object is created without any related configuration object, it will not - trigger this rule. + If a device object is created without any related configuration + object, it will not trigger this rule. -Creating a new *"Device"* rule will also automatically provision subnets and IP -addresses for existing devices within the organization. +Creating a new *"Device"* rule will also automatically provision subnets +and IP addresses for existing devices within the organization. .. _vpn_rule: VPN Subnet Division Rule ~~~~~~~~~~~~~~~~~~~~~~~~ -This rule triggers when a template flagged as *VPN-client* is assigned to a device -configuration, but only if the VPN server associated with the VPN-client template uses -the same subnet to which the subnet division rule is assignated to. +This rule triggers when a template flagged as *VPN-client* is assigned to +a device configuration, but only if the VPN server associated with the +VPN-client template uses the same subnet to which the subnet division rule +is assignated to. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/subnet-division-rule.png @@ -73,8 +74,8 @@ In this example, **VPN subnet division rule** is used. 2. Create a VPN Server ---------------------- -Now create a VPN Server and choose the previously created **master subnet** as the -subnet for this VPN Server. +Now create a VPN Server and choose the previously created **master +subnet** as the subnet for this VPN Server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-server.png @@ -83,95 +84,105 @@ subnet for this VPN Server. 3. Create a VPN Client Template ------------------------------- -Create a template, setting the **Type** field to **VPN Client** and **VPN** field to use -the previously created VPN Server. +Create a template, setting the **Type** field to **VPN Client** and +**VPN** field to use the previously created VPN Server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/vpn-client.png :alt: Creating a VPN Client template example -**Note**: You can also check the **Enable by default** field if you want to -automatically apply this template to devices that will register in future. +**Note**: You can also check the **Enable by default** field if you want +to automatically apply this template to devices that will register in +future. 4. Apply VPN Client Template to Devices --------------------------------------- -With everything in place, you can now apply the VPN Client Template to devices. +With everything in place, you can now apply the VPN Client Template to +devices. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/apply-template-to-device.png :alt: Adding template to device example -After saving the device, you should see all provisioned Subnets and IPs for this device -under :ref:`System Defined Variables `. +After saving the device, you should see all provisioned Subnets and IPs +for this device under :ref:`System Defined Variables +`. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/subnet-division-rule/system-defined-variables.png :alt: Provisioned Subnets and IPs available as System Defined Variables example -You can now use these :doc:`variables` in the configuration of devices of your network. +You can now use these :doc:`variables` in the configuration of devices of +your network. Important notes for using Subnet Division ----------------------------------------- -- In the example provided, the Subnet, VPN Server, and VPN Client Template were - associated with the **default** organization. You can also utilize **Systemwide - Shared** Subnet, VPN Server, or VPN Client Template; however, remember that the Subnet - Division Rule will always be linked to an organization. It will only be triggered when - a VPN Client Template is applied to a Device with the same organization as the Subnet - Division Rule. -- Configuration variables can be used for provisioned subnets and IPs in the Template. - Each variable will resolve differently for different devices. For example, - ``OW_subnet1_ip1`` will resolve to ``10.0.0.1`` for one device and ``10.0.0.55`` for - another. Every device receives its own set of subnets and IPs. Ensure to provide - default fallback values in the *default values* template field (mainly used for - validation). -- The Subnet Division Rule automatically creates a reserved subnet, which can be - utilized to provision any IP addresses that need to be created manually. The remaining - address space of the master subnet must not be interfered with, or the automation - implemented in this module will not function. -- The example provided used the :ref:`VPN subnet division rule `. Similarly, - the :ref:`device subnet division rule ` can be employed, requiring only - :ref:`the creation of a subnet and a subnet division rule `. +- In the example provided, the Subnet, VPN Server, and VPN Client Template + were associated with the **default** organization. You can also utilize + **Systemwide Shared** Subnet, VPN Server, or VPN Client Template; + however, remember that the Subnet Division Rule will always be linked to + an organization. It will only be triggered when a VPN Client Template is + applied to a Device with the same organization as the Subnet Division + Rule. +- Configuration variables can be used for provisioned subnets and IPs in + the Template. Each variable will resolve differently for different + devices. For example, ``OW_subnet1_ip1`` will resolve to ``10.0.0.1`` + for one device and ``10.0.0.55`` for another. Every device receives its + own set of subnets and IPs. Ensure to provide default fallback values in + the *default values* template field (mainly used for validation). +- The Subnet Division Rule automatically creates a reserved subnet, which + can be utilized to provision any IP addresses that need to be created + manually. The remaining address space of the master subnet must not be + interfered with, or the automation implemented in this module will not + function. +- The example provided used the :ref:`VPN subnet division rule + `. Similarly, the :ref:`device subnet division rule + ` can be employed, requiring only :ref:`the creation of a + subnet and a subnet division rule `. Limitations of Subnet Division Rules ------------------------------------ -In the current implementation, it is not possible to change *Size*, *Number of Subnets* -and *Number of IPs* fields of an existing subnet division rule due to following reasons: +In the current implementation, it is not possible to change *Size*, +*Number of Subnets* and *Number of IPs* fields of an existing subnet +division rule due to following reasons: Size ~~~~ -Allowing to change size of provisioned subnets of an existing subnet division rule will -require rebuilding of Subnets and IP addresses which has possibility of breaking -existing configurations. +Allowing to change size of provisioned subnets of an existing subnet +division rule will require rebuilding of Subnets and IP addresses which +has possibility of breaking existing configurations. Number of Subnets ~~~~~~~~~~~~~~~~~ -Allowing to decrease number of subnets of an existing subnet division rule can create -patches of unused subnets dispersed everywhere in the master subnet. Allowing to -increase number of subnets will break the continuous allocation of subnets for every -device. It can also break configuration of devices. +Allowing to decrease number of subnets of an existing subnet division rule +can create patches of unused subnets dispersed everywhere in the master +subnet. Allowing to increase number of subnets will break the continuous +allocation of subnets for every device. It can also break configuration of +devices. Number of IPs ~~~~~~~~~~~~~ -**Decreasing the number of IPs** in an existing subnet division rule is not allowed as -it can lead to deletion of IP addresses, potentially breaking configurations of existing -devices. +**Decreasing the number of IPs** in an existing subnet division rule is +not allowed as it can lead to deletion of IP addresses, potentially +breaking configurations of existing devices. **Increasing the number of IPs is allowed**. -If you need to modify any of these fields (**Size**, **Number of Subnets**, or **Number -of IPs**), we recommend to proceed as follows: +If you need to modify any of these fields (**Size**, **Number of +Subnets**, or **Number of IPs**), we recommend to proceed as follows: 1. Delete the existing rule. 2. Create a new rule. -The automation will provision new subnets and addresses according to the new parameters -to any existing devices that are eligible to the subnet division rule. +The automation will provision new subnets and addresses according to the +new parameters to any existing devices that are eligible to the subnet +division rule. -However, be aware that existing devices **will probably be reassigned different subnets -and IP addresses** than the ones used previously. +However, be aware that existing devices **will probably be reassigned +different subnets and IP addresses** than the ones used previously. diff --git a/docs/user/templates.rst b/docs/user/templates.rst index 564fc9c06..375e708ff 100644 --- a/docs/user/templates.rst +++ b/docs/user/templates.rst @@ -8,23 +8,24 @@ Configuration Templates What is a Template? ------------------- -Templates are designed to store configuration that can be reused by some or all the -devices in the system. +Templates are designed to store configuration that can be reused by some +or all the devices in the system. -Updating the configuration stored in a template allows to update the configuration of -all the devices that have that template assigned. +Updating the configuration stored in a template allows to update the +configuration of all the devices that have that template assigned. -This means that configuration can be defined only once for multiple devices, and if the -need to update a specific piece of configuration arises, it can be easily achieved by -updating the template. +This means that configuration can be defined only once for multiple +devices, and if the need to update a specific piece of configuration +arises, it can be easily achieved by updating the template. Template Ordering and Override ------------------------------ -A device can use multiple templates, **the order in which templates are assigned to each -device matters**: templates assigned last can override templates assigned earlier, the -order can be changed by drag and dropping the template in the device configuration page -as in the animated screenshot below. +A device can use multiple templates, **the order in which templates are +assigned to each device matters**: templates assigned last can override +templates assigned earlier, the order can be changed by drag and dropping +the template in the device configuration page as in the animated +screenshot below. .. image:: /images/templates/template-ordering.gif :target: ../../_images/template-ordering.gif @@ -33,31 +34,33 @@ as in the animated screenshot below. The device configuration can also override what is defined in templates. -Overriding means redefining a specific configuration section in a way that overwrites -the template. +Overriding means redefining a specific configuration section in a way that +overwrites the template. -**Overriding involves some form of duplication of information, which is not great, it -should be used as a last resort**. The recommended way to define parts of the -configuration that are specific for each device is to use :doc:`Configuration variables -<./variables>`. +**Overriding involves some form of duplication of information, which is +not great, it should be used as a last resort**. The recommended way to +define parts of the configuration that are specific for each device is to +use :doc:`Configuration variables <./variables>`. .. _controller_shared_vs_org: Shared Templates vs Organization Specific ----------------------------------------- -Templates can be *organization specific* or *shared* (no organization specified). +Templates can be *organization specific* or *shared* (no organization +specified). .. image:: /images/templates/organization-specific-vs-shared.gif :target: ../../_images/organization-specific-vs-shared.gif :align: center :alt: Shared templates vs organization specific -**Organization specific templates** will be available and usable only within the same -organization which they are assigned to. +**Organization specific templates** will be available and usable only +within the same organization which they are assigned to. -If no organization is specified when creating a template, a shared template will be -created, **shared templates are available to any organization in the system**. +If no organization is specified when creating a template, a shared +template will be created, **shared templates are available to any +organization in the system**. Here are a few typical use cases of shared templates: @@ -75,25 +78,28 @@ Default Templates :align: center :alt: Templates enabled by default -When templates are flagged as **"Enabled by default"**, they will be automatically -assigned to new devices. +When templates are flagged as **"Enabled by default"**, they will be +automatically assigned to new devices. -This is a very powerful feature: **once default templates are correctly configured to -implement the use case you need, you will only have to register a device into OpenWISP -for it to auto-configure itself**. +This is a very powerful feature: **once default templates are correctly +configured to implement the use case you need, you will only have to +register a device into OpenWISP for it to auto-configure itself**. -Moreover, you can change the default templates any time you need, which is the reason -this feature has replaced the practice of storing default configuration in firmware -images (which would need to be recompiled and redistributed): with default templates, -the default firmware image only needs to contain the bare minimum configuration to -connect to OpenWISP, once the device connects to OpenWISP it will download and apply the -default templates without the need of manual intervention from the network operators. +Moreover, you can change the default templates any time you need, which is +the reason this feature has replaced the practice of storing default +configuration in firmware images (which would need to be recompiled and +redistributed): with default templates, the default firmware image only +needs to contain the bare minimum configuration to connect to OpenWISP, +once the device connects to OpenWISP it will download and apply the +default templates without the need of manual intervention from the network +operators. -An organization specific template flagged as default will be automatically assigned to -any new device which will be created in the same organization. +An organization specific template flagged as default will be automatically +assigned to any new device which will be created in the same organization. -A shared default template instead will be automatically assigned to all the new devices -which will be created in the system, regardless of organization. +A shared default template instead will be automatically assigned to all +the new devices which will be created in the system, regardless of +organization. .. _required_templates: @@ -104,23 +110,24 @@ Required Templates :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/required-templates.png :alt: Required template example -Required templates are similar to :ref:`default_templates` but cannot be unassigned from -a device configuration, they can only be overridden. +Required templates are similar to :ref:`default_templates` but cannot be +unassigned from a device configuration, they can only be overridden. -They will be always assigned earlier than default templates, so they can be overridden -if needed. +They will be always assigned earlier than default templates, so they can +be overridden if needed. -In the example above, the "SSID" template is flagged as "(required)" and its checkbox is -always checked and disabled. +In the example above, the "SSID" template is flagged as "(required)" and +its checkbox is always checked and disabled. Device Group Templates ---------------------- -:ref:`default_templates` are an incredibly useful tool, but they're limited: **only one -set of default templates can be created** per each organization. +:ref:`default_templates` are an incredibly useful tool, but they're +limited: **only one set of default templates can be created** per each +organization. -With :ref:`device_group_templates` it is possible to specify a set of default templates -for each device group. +With :ref:`device_group_templates` it is possible to specify a set of +default templates for each device group. .. _templates_tags: @@ -132,27 +139,30 @@ Template Tags :align: center :alt: Template tags -In some cases, you may have multiple set of default settings to use, let's explain this -with a practical example: you may have 2 different device types in your network: +In some cases, you may have multiple set of default settings to use, let's +explain this with a practical example: you may have 2 different device +types in your network: -- Mesh routers: they connect to one another, forming a wireless mesh network -- Dumb access points: they connect to the mesh routers on the LAN port and offer - internet access which is routed via the mesh network by the routers +- Mesh routers: they connect to one another, forming a wireless mesh + network +- Dumb access points: they connect to the mesh routers on the LAN port and + offer internet access which is routed via the mesh network by the + routers -In this example case, the default configuration to use in each device type can greatly -differ. +In this example case, the default configuration to use in each device type +can greatly differ. -In such a setup, default templates would only contain configuration which is common to -both device types, while configuration which is specific for each type would be stored -in specific templates which are then tagged with specific keywords: +In such a setup, default templates would only contain configuration which +is common to both device types, while configuration which is specific for +each type would be stored in specific templates which are then tagged with +specific keywords: - ``mesh``: tag to use for mesh configuration - ``dumb-ap``: tag to use for dumb AP configuration -The :ref:`openwisp-config -` configuration of -each device type must specify the correct tag before each device registers in the -system. +The :ref:`openwisp-config ` +configuration of each device type must specify the correct tag before each +device registers in the system. Here's the sample ``/etc/config/openwisp`` configuration for mesh devices: @@ -163,16 +173,17 @@ Here's the sample ``/etc/config/openwisp`` configuration for mesh devices: option shared_secret 'mySharedSecret123' option tags 'mesh' -Once devices with the above configuration will register into the system, any template -tagged as ``mesh`` (as in the screenshot below) will be assigned to them. +Once devices with the above configuration will register into the system, +any template tagged as ``mesh`` (as in the screenshot below) will be +assigned to them. .. image:: /images/templates/mesh-template-tag.png :target: ../../_images/mesh-template-tag.png :align: center :alt: Template tags: mesh example -The sample ``/etc/config/openwisp`` configuration for dumb access points is the -following: +The sample ``/etc/config/openwisp`` configuration for dumb access points +is the following: .. code-block:: @@ -181,8 +192,9 @@ following: option shared_secret 'mySharedSecret123' option tags 'dumb-ap' -Once devices with the above configuration will register into the system, any template -tagged as ``dumb-ap`` (as in the screenshot below) will be assigned to them. +Once devices with the above configuration will register into the system, +any template tagged as ``dumb-ap`` (as in the screenshot below) will be +assigned to them. .. image:: /images/templates/dumb-ap-template-tag.png :target: ../../_images/dumb-ap-template-tag.png @@ -192,9 +204,9 @@ tagged as ``dumb-ap`` (as in the screenshot below) will be assigned to them. Implementation Details of Templates ----------------------------------- -Templates are implemented under the hood by the OpenWISP configuration engine: -netjsonconfig. +Templates are implemented under the hood by the OpenWISP configuration +engine: netjsonconfig. -For more advanced technical information about templates, consult the netjsonconfig -documentation: `Basic Concepts, Template +For more advanced technical information about templates, consult the +netjsonconfig documentation: `Basic Concepts, Template `_. diff --git a/docs/user/variables.rst b/docs/user/variables.rst index ed0794c77..ce2043551 100644 --- a/docs/user/variables.rst +++ b/docs/user/variables.rst @@ -1,19 +1,20 @@ Configuration Variables ======================= -Sometimes the configuration is not exactly equal on all the devices, some parameters are -unique to each device or need to be changed by the user. +Sometimes the configuration is not exactly equal on all the devices, some +parameters are unique to each device or need to be changed by the user. -In these cases it is possible to use configuration variables in conjunction with -templates, this feature is also known as *configuration context*, think of it like a -dictionary which is passed to the function which renders the configuration, so that it -can fill variables according to the passed context. +In these cases it is possible to use configuration variables in +conjunction with templates, this feature is also known as *configuration +context*, think of it like a dictionary which is passed to the function +which renders the configuration, so that it can fill variables according +to the passed context. Different Types of Variables ---------------------------- -The different ways in which variables are defined are described below in the order (high -to low) of their precedence. +The different ways in which variables are defined are described below in +the order (high to low) of their precedence. .. contents:: :depth: 2 @@ -24,9 +25,9 @@ to low) of their precedence. 1. User Defined Device Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the device configuration section you can find a section named "Configuration -variables" where it is possible to define the configuration variables and their values, -as shown in the example below: +In the device configuration section you can find a section named +"Configuration variables" where it is possible to define the configuration +variables and their values, as shown in the example below: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/device-context.png @@ -35,7 +36,8 @@ as shown in the example below: 2. Predefined Device Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each device gets the following attributes passed as configuration variables: +Each device gets the following attributes passed as configuration +variables: - ``id`` - ``key`` @@ -55,8 +57,8 @@ Refer to :ref:`device_group_variables` for more information. Variables can also be defined at the organization level. You can set the *organization variables* from the organization change page -``/admin/openwisp_users/organization//change/``, under the -**Configuration Management Settings**. +``/admin/openwisp_users/organization//change/``, under +the **Configuration Management Settings**. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/organization-variables.png @@ -65,26 +67,28 @@ You can set the *organization variables* from the organization change page 5. Global Variables ~~~~~~~~~~~~~~~~~~~ -Variables can also be defined globally using the :ref:`context_setting` setting, see -also :doc:`How to Edit Django Settings <../../../../user/django-settings>`. +Variables can also be defined globally using the :ref:`context_setting` +setting, see also :doc:`How to Edit Django Settings +<../../../../user/django-settings>`. 6. Template Default Values ~~~~~~~~~~~~~~~~~~~~~~~~~~ -It's possible to specify the default values of variables defined in a template. +It's possible to specify the default values of variables defined in a +template. This allows to achieve 2 goals: -1. pass schema validation without errors (otherwise it would not be possible to save the - template in the first place) -2. provide good default values that are valid in most cases but can be overridden in the - device if needed +1. pass schema validation without errors (otherwise it would not be + possible to save the template in the first place) +2. provide good default values that are valid in most cases but can be + overridden in the device if needed -These default values will be overridden by the :ref:`User defined device variables -`. +These default values will be overridden by the :ref:`User defined device +variables `. -The default values of variables can be manipulated from the section "configuration -variables" in the edit template page: +The default values of variables can be manipulated from the section +"configuration variables" in the edit template page: .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/template-default-values.png @@ -95,9 +99,10 @@ variables" in the edit template page: 7. System Defined Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Predefined device variables, global variables and other variables that are automatically -managed by the system (eg: when using templates of type VPN-client) are displayed in the -admin UI as *System Defined Variables* in read-only mode. +Predefined device variables, global variables and other variables that are +automatically managed by the system (eg: when using templates of type +VPN-client) are displayed in the admin UI as *System Defined Variables* in +read-only mode. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/system-defined-variables.png @@ -106,10 +111,11 @@ admin UI as *System Defined Variables* in read-only mode. Example Usage of Variables -------------------------- -Here's a typical use case, the WiFi SSID and WiFi password. You don't want to define -this for every device, but you may want to allow operators to easily change the SSID or -WiFi password for a specific device without having to re-define the whole wifi interface -to avoid duplicating information. +Here's a typical use case, the WiFi SSID and WiFi password. You don't want +to define this for every device, but you may want to allow operators to +easily change the SSID or WiFi password for a specific device without +having to re-define the whole wifi interface to avoid duplicating +information. This would be the template: @@ -156,9 +162,10 @@ The default values can then be overridden at :ref:`device level Implementation Details of Variables ----------------------------------- -Variables are implemented under the hood by the OpenWISP configuration engine: -netjsonconfig. +Variables are implemented under the hood by the OpenWISP configuration +engine: netjsonconfig. -For more advanced technical information about variables, consult the netjsonconfig -documentation: `Basic Concepts, Context (configuration variables) +For more advanced technical information about variables, consult the +netjsonconfig documentation: `Basic Concepts, Context (configuration +variables) `_. diff --git a/docs/user/vxlan-wireguard.rst b/docs/user/vxlan-wireguard.rst index 6756a88e7..719813f38 100644 --- a/docs/user/vxlan-wireguard.rst +++ b/docs/user/vxlan-wireguard.rst @@ -1,8 +1,9 @@ Automating VXLAN over WireGuard Tunnels ======================================= -By following these steps, you will be able to setup layer 2 VXLAN tunnels encapsulated -in `WireGuard `_ tunnels which work on layer 3. +By following these steps, you will be able to setup layer 2 VXLAN tunnels +encapsulated in `WireGuard `_ tunnels which +work on layer 3. .. include:: ../partials/shared-object.rst @@ -14,27 +15,29 @@ in `WireGuard `_ tunnels which work on layer 3. ----------------------------------------------------------- 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. We will set **Name** of this VPN server ``Wireguard VXLAN`` and **Host** as - ``wireguard-vxlan-server.mydomain.com`` (update this to point to your WireGuard VXLAN - VPN server). +2. We will set **Name** of this VPN server ``Wireguard VXLAN`` and + **Host** as ``wireguard-vxlan-server.mydomain.com`` (update this to + point to your WireGuard VXLAN VPN server). 3. Select ``VXLAN over WireGuard`` from the dropdown as **VPN Backend**. -4. When using VXLAN over WireGuard, OpenWISP takes care of managing IP addresses - (assigning an IP address to each VPN peer). You can create a new subnet or select an - existing one from the dropdown menu. You can also assign an **Internal IP** to the - WireGuard Server or leave it empty for OpenWISP to configure. This IP address will be - used by the WireGuard interface on server. +4. When using VXLAN over WireGuard, OpenWISP takes care of managing IP + addresses (assigning an IP address to each VPN peer). You can create a + new subnet or select an existing one from the dropdown menu. You can + also assign an **Internal IP** to the WireGuard Server or leave it + empty for OpenWISP to configure. This IP address will be used by the + WireGuard interface on server. 5. We have set the **Webhook Endpoint** as - ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` for this example. - You will need to update this according to you VPN upgrader endpoint. Set **Webhook - AuthToken** to any strong passphrase, this will be used to ensure that configuration - upgrades are requested from trusted sources. + ``https://wireguard-vxlan-server.mydomain.com:8081/trigger-update`` for + this example. You will need to update this according to you VPN + upgrader endpoint. Set **Webhook AuthToken** to any strong passphrase, + this will be used to ensure that configuration upgrades are requested + from trusted sources. - **Note**: If you are following this tutorial for also setting up WireGuard VPN - server, just substitute ``wireguard-server.mydomain.com`` with hostname of your VPN - server and follow the steps in next section. + **Note**: If you are following this tutorial for also setting up + WireGuard VPN server, just substitute ``wireguard-server.mydomain.com`` + with hostname of your VPN server and follow the steps in next section. -6. Under the configuration section, set the name of WireGuard tunnel 1 interface. We - have used ``wg0`` in this example. +6. Under the configuration section, set the name of WireGuard tunnel 1 + interface. We have used ``wg0`` in this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-1.png @@ -44,9 +47,10 @@ in `WireGuard `_ tunnels which work on layer 3. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-2.png :alt: WireGuard VPN VXLAN server configuration example 2 -7. After clicking on **Save and continue editing**, you will see that OpenWISP has - automatically created public and private key for WireGuard server in **System Defined - Variables** along with internal IP address information. +7. After clicking on **Save and continue editing**, you will see that + OpenWISP has automatically created public and private key for WireGuard + server in **System Defined Variables** along with internal IP address + information. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/vpn-server-3.png @@ -55,32 +59,34 @@ in `WireGuard `_ tunnels which work on layer 3. 2. Deploy Wireguard VXLAN VPN Server ------------------------------------ -If you haven't already set up WireGuard on your VPN server, this is a good time to do -so. We recommend using the `ansible-wireguard-openwisp -`_ role for installing WireGuard -since it also installs scripts that allow OpenWISP to manage the WireGuard VPN server -along with VXLAN tunnels. +If you haven't already set up WireGuard on your VPN server, this is a good +time to do so. We recommend using the `ansible-wireguard-openwisp +`_ role for +installing WireGuard since it also installs scripts that allow OpenWISP to +manage the WireGuard VPN server along with VXLAN tunnels. -Pay attention to the VPN server attributes used in your playbook. It should be the same -as the VPN server configuration in OpenWISP. +Pay attention to the VPN server attributes used in your playbook. It +should be the same as the VPN server configuration in OpenWISP. 3. Create VPN Client Template for WireGuard VXLAN VPN Server ------------------------------------------------------------ 1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you want) and select - ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can be - applied to. For this example, we will leave it as ``OpenWrt``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is - ``Wireguard VXLAN``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP - automatically generate public and private keys and provision IP addresses for each - WireGuard VPN client along with the VXLAN Network Identifier (VNI). -6. After clicking on **Save and continue editing** button, you will see details of the - *Wireguard VXLAN* VPN server in **System Defined Variables**. The template - configuration will be automatically generated which you can tweak accordingly. We - will use the automatically generated VPN client configuration for this example. +2. Set ``Wireguard VXLAN Client`` as **Name** (you can set whatever you + want) and select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template + can be applied to. For this example, we will leave it as ``OpenWrt``. +4. Select the correct VPN server from the dropdown for the **VPN** field. + Here it is ``Wireguard VXLAN``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will + make OpenWISP automatically generate public and private keys and + provision IP addresses for each WireGuard VPN client along with the + VXLAN Network Identifier (VNI). +6. After clicking on **Save and continue editing** button, you will see + details of the *Wireguard VXLAN* VPN server in **System Defined + Variables**. The template configuration will be automatically generated + which you can tweak accordingly. We will use the automatically + generated VPN client configuration for this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/template.png @@ -91,26 +97,28 @@ as the VPN server configuration in OpenWISP. .. note:: - This step assumes that you already have a device registered on OpenWISP. Register or - create a device before proceeding. + This step assumes that you already have a device registered on + OpenWISP. Register or create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *WireGuard VXLAN Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some entries in - **System Defined Variables**. It will contain internal IP address, private and public - key for the WireGuard client on the device and details of WireGuard VPN server along - with VXLAN Network Identifier(VNI) of this device. +3. Upon clicking on **Save and continue editing** button, you will see + some entries in **System Defined Variables**. It will contain internal + IP address, private and public key for the WireGuard client on the + device and details of WireGuard VPN server along with VXLAN Network + Identifier(VNI) of this device. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-vxlan-tutorial/device-configuration.png :alt: WireGuard VXLAN VPN device configuration example -**Voila!** You have successfully configured OpenWISP to manage VXLAN over WireGuard -tunnels for your devices. +**Voila!** You have successfully configured OpenWISP to manage VXLAN over +WireGuard tunnels for your devices. .. seealso:: - You may also want to explore other automated VPN tunnel provisioning options: + You may also want to explore other automated VPN tunnel provisioning + options: - :doc:`Wireguard ` - :doc:`Zerotier ` diff --git a/docs/user/wireguard.rst b/docs/user/wireguard.rst index 0c6f63347..5e99668d7 100644 --- a/docs/user/wireguard.rst +++ b/docs/user/wireguard.rst @@ -1,8 +1,8 @@ Automating WireGuard Tunnels ============================ -This guide will help you to set up the automatic provisioning of `WireGuard -`_ tunnels for your devices. +This guide will help you to set up the automatic provisioning of +`WireGuard `_ tunnels for your devices. .. include:: ../partials/shared-object.rst @@ -14,27 +14,28 @@ This guide will help you to set up the automatic provisioning of `WireGuard ------------------------------------------------ 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. -2. Set the **Name** of this VPN server as ``WireGuard`` and the **Host** as - ``wireguard-server.mydomain.com`` (update this to point to your WireGuard VPN - server). +2. Set the **Name** of this VPN server as ``WireGuard`` and the **Host** + as ``wireguard-server.mydomain.com`` (update this to point to your + WireGuard VPN server). 3. Select ``WireGuard`` from the dropdown as the **VPN Backend**. -4. When using WireGuard, OpenWISP takes care of managing IP addresses, assigning an IP - address to each VPN peer. Create a new subnet or select an existing one from the - dropdown menu. You can also assign an **Internal IP** to the WireGuard Server or - leave it empty for OpenWISP to configure. This IP address will be used by the - WireGuard interface on the server. +4. When using WireGuard, OpenWISP takes care of managing IP addresses, + assigning an IP address to each VPN peer. Create a new subnet or select + an existing one from the dropdown menu. You can also assign an + **Internal IP** to the WireGuard Server or leave it empty for OpenWISP + to configure. This IP address will be used by the WireGuard interface + on the server. 5. Set the **Webhook Endpoint** as - ``https://wireguard-server.mydomain.com:8081/trigger-update`` for this example. - Update this according to your VPN upgrader endpoint. Set **Webhook AuthToken** to any - strong passphrase; this will be used to ensure that configuration upgrades are - requested from trusted sources. + ``https://wireguard-server.mydomain.com:8081/trigger-update`` for this + example. Update this according to your VPN upgrader endpoint. Set + **Webhook AuthToken** to any strong passphrase; this will be used to + ensure that configuration upgrades are requested from trusted sources. **Note**: If you are setting up a WireGuard VPN server, substitute - ``wireguard-server.mydomain.com`` with the hostname of your VPN server and follow the - steps in the next section. + ``wireguard-server.mydomain.com`` with the hostname of your VPN server + and follow the steps in the next section. -6. Under the configuration section, set the name of the WireGuard tunnel 1 interface. In - this example, we have used ``wg0``. +6. Under the configuration section, set the name of the WireGuard tunnel 1 + interface. In this example, we have used ``wg0``. .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-1.png @@ -44,9 +45,10 @@ This guide will help you to set up the automatic provisioning of `WireGuard :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-2.png :alt: WireGuard VPN server configuration example 2 -7. After clicking on **Save and continue editing**, you will see that OpenWISP has - automatically created public and private keys for the WireGuard server in **System - Defined Variables**, along with internal IP address information. +7. After clicking on **Save and continue editing**, you will see that + OpenWISP has automatically created public and private keys for the + WireGuard server in **System Defined Variables**, along with internal + IP address information. .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/vpn-server-3.png @@ -55,34 +57,35 @@ This guide will help you to set up the automatic provisioning of `WireGuard 2. Deploy WireGuard VPN Server ------------------------------ -If you haven't already set up WireGuard on your VPN server, this would be a good time to -do so. +If you haven't already set up WireGuard on your VPN server, this would be +a good time to do so. We recommend using the `ansible-wireguard-openwisp -`_ role for installing -WireGuard, as it also installs scripts that allow OpenWISP to manage the WireGuard VPN -server. +`_ role for +installing WireGuard, as it also installs scripts that allow OpenWISP to +manage the WireGuard VPN server. -Ensure that the VPN server attributes used in your playbook match the VPN server -configuration in OpenWISP. +Ensure that the VPN server attributes used in your playbook match the VPN +server configuration in OpenWISP. 3. Create VPN Client Template for WireGuard VPN Server ------------------------------------------------------ 1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``WireGuard Client`` as **Name** (you can set whatever you want) and select - ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can be - applied to. For this example, we will leave it to ``OpenWrt``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is - ``WireGuard``. -5. Ensure that **Automatic tunnel provisioning** is checked. This will make OpenWISP to - automatically generate public and private keys and provision IP address for each - WireGuard VPN client. -6. After clicking on **Save and continue editing** button, you will see details of - *WireGuard* VPN server in **System Defined Variables**. The template configuration - will be automatically generated which you can tweak accordingly. We will use the - automatically generated VPN client configuration for this example. +2. Set ``WireGuard Client`` as **Name** (you can set whatever you want) + and select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template + can be applied to. For this example, we will leave it to ``OpenWrt``. +4. Select the correct VPN server from the dropdown for the **VPN** field. + Here it is ``WireGuard``. +5. Ensure that **Automatic tunnel provisioning** is checked. This will + make OpenWISP to automatically generate public and private keys and + provision IP address for each WireGuard VPN client. +6. After clicking on **Save and continue editing** button, you will see + details of *WireGuard* VPN server in **System Defined Variables**. The + template configuration will be automatically generated which you can + tweak accordingly. We will use the automatically generated VPN client + configuration for this example. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/template.png @@ -93,27 +96,27 @@ configuration in OpenWISP. .. note:: - This step assumes that you already have a device registered on OpenWISP. Register or - create a device before proceeding. + This step assumes that you already have a device registered on + OpenWISP. Register or create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *WireGuard Client* template. -3. Upon clicking on **Save and continue editing** button, you will see some entries in - **System Defined Variables**. It will contain internal IP address, private and public - key for the WireGuard client on the device along with details of WireGuard VPN - server. +3. Upon clicking on **Save and continue editing** button, you will see + some entries in **System Defined Variables**. It will contain internal + IP address, private and public key for the WireGuard client on the + device along with details of WireGuard VPN server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/wireguard-tutorial/device-configuration.png :alt: WireGuard VPN device configuration example -**Voila!** You have successfully configured OpenWISP to manage WireGuard tunnels for -your devices. - +**Voila!** You have successfully configured OpenWISP to manage WireGuard +tunnels for your devices. .. seealso:: - You may also want to explore other automated VPN tunnel provisioning options: + You may also want to explore other automated VPN tunnel provisioning + options: - :doc:`Wireguard over VXLAN ` - :doc:`Zerotier ` diff --git a/docs/user/zerotier.rst b/docs/user/zerotier.rst index ffd912257..54249f0af 100644 --- a/docs/user/zerotier.rst +++ b/docs/user/zerotier.rst @@ -16,8 +16,8 @@ Automating ZeroTier Tunnels

-Follow the procedure described below to set up `ZeroTier `_ -tunnels on your devices. +Follow the procedure described below to set up `ZeroTier +`_ tunnels on your devices. .. include:: ../partials/shared-object.rst @@ -28,25 +28,29 @@ tunnels on your devices. 1. Configure Self-Hosted ZeroTier Network Controller ---------------------------------------------------- -If you haven't already set up a self-hosted ZeroTier network controller on your server, -now is a good time to do so. You can start by simply installing ZeroTier on your server -from the `official website `_. +If you haven't already set up a self-hosted ZeroTier network controller on +your server, now is a good time to do so. You can start by simply +installing ZeroTier on your server from the `official website +`_. 2. Create VPN Server Configuration for ZeroTier ----------------------------------------------- 1. Visit ``/admin/config/vpn/add/`` to add a new VPN server. 2. We will set **Name** of this VPN server ``ZeroTier`` and **Host** as - ``my-zerotier-server.mydomain.com:9993`` (update this to point to your ZeroTier VPN - server). + ``my-zerotier-server.mydomain.com:9993`` (update this to point to your + ZeroTier VPN server). 3. Select ``ZeroTier`` from the dropdown as **VPN Backend**. -4. When using ZeroTier, OpenWISP takes care of managing IP addresses (assigning an IP - address to each VPN client (ZeroTier network members)). You can create a new subnet - or select an existing one from the dropdown menu. You can also assign an **Internal - IP** to the ZeroTier controller or leave it empty for OpenWISP to configure. This IP - address will be used to assign it to the ZeroTier controller running on the server. -5. Set the **Webhook AuthToken**, this will be the ZeroTier authorization token which - you can obtain by running the following command on the ZeroTier controller: +4. When using ZeroTier, OpenWISP takes care of managing IP addresses + (assigning an IP address to each VPN client (ZeroTier network + members)). You can create a new subnet or select an existing one from + the dropdown menu. You can also assign an **Internal IP** to the + ZeroTier controller or leave it empty for OpenWISP to configure. This + IP address will be used to assign it to the ZeroTier controller running + on the server. +5. Set the **Webhook AuthToken**, this will be the ZeroTier authorization + token which you can obtain by running the following command on the + ZeroTier controller: .. code-block:: shell @@ -68,10 +72,11 @@ from the `official website `_. :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-4.png :alt: ZeroTier VPN server configuration example 4 -6. After clicking on **Save and continue editing**, OpenWISP automatically detects the - node address of the ZeroTier controller and creates a ZeroTier network. The - **network_id** of this network can be viewed in the **System Defined Variables** - section, where it also provides internal IP address information. +6. After clicking on **Save and continue editing**, OpenWISP automatically + detects the node address of the ZeroTier controller and creates a + ZeroTier network. The **network_id** of this network can be viewed in + the **System Defined Variables** section, where it also provides + internal IP address information. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/vpn-server-5.png @@ -81,26 +86,29 @@ from the `official website `_. ----------------------------------------------------- 1. Visit ``/admin/config/template/add/`` to add a new template. -2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and select - ``VPN-client`` as **type** from the dropdown list. -3. The **Backend** field refers to the backend of the device this template can be - applied to. For this example, we will leave it to ``OpenWrt``. -4. Select the correct VPN server from the dropdown for the **VPN** field. Here it is - ``ZeroTier``. -5. Ensure that the **Automatic tunnel provisioning** option is checked. This will enable - OpenWISP to automatically provision an IP address and ZeroTier identity secrets (used - for assigning member IDs) for each ZeroTier VPN client. -6. After clicking on **Save and continue editing** button, you will see details of - *ZeroTier* VPN server in **System Defined Variables**. The template configuration - will be automatically generated which you can tweak accordingly. We will use the - automatically generated VPN client configuration for this example. +2. Set ``ZeroTier Client`` as **Name** (you can set whatever you want) and + select ``VPN-client`` as **type** from the dropdown list. +3. The **Backend** field refers to the backend of the device this template + can be applied to. For this example, we will leave it to ``OpenWrt``. +4. Select the correct VPN server from the dropdown for the **VPN** field. + Here it is ``ZeroTier``. +5. Ensure that the **Automatic tunnel provisioning** option is checked. + This will enable OpenWISP to automatically provision an IP address and + ZeroTier identity secrets (used for assigning member IDs) for each + ZeroTier VPN client. +6. After clicking on **Save and continue editing** button, you will see + details of *ZeroTier* VPN server in **System Defined Variables**. The + template configuration will be automatically generated which you can + tweak accordingly. We will use the automatically generated VPN client + configuration for this example. .. note:: OpenWISP uses `zerotier-idtool - `_ to - manage **ZeroTier identity secrets**. Please make sure that you have `ZeroTier - package installed `_ on the server. + `_ + to manage **ZeroTier identity secrets**. Please make sure that you + have `ZeroTier package installed + `_ on the server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/template.png @@ -111,35 +119,37 @@ from the `official website `_. .. note:: - This step assumes that you already have a device registered on OpenWISP. Register or - create a device before proceeding. + This step assumes that you already have a device registered on + OpenWISP. Register or create a device before proceeding. 1. Open the **Configuration** tab of the concerned device. 2. Select the *ZeroTier Client* template. -3. Upon clicking the **Save and Continue Editing** button, you will see entries in the - **System Defined Variables** section. These entries will include - **zerotier_member_id**, **identity_secret**, and the internal **IP address** of the - ZeroTier client (network member) on the device, along with details of the VPN server. +3. Upon clicking the **Save and Continue Editing** button, you will see + entries in the **System Defined Variables** section. These entries will + include **zerotier_member_id**, **identity_secret**, and the internal + **IP address** of the ZeroTier client (network member) on the device, + along with details of the VPN server. .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-1.png :alt: ZeroTier VPN device configuration example 1 -4. Once the configuration is successfully applied to the device, you will notice a new - ZeroTier interface that is up and running. This interface will have the name - ``owzt89f498`` (where ``owzt`` is followed by the last six hexadecimal characters of - the ZeroTier **network ID**). +4. Once the configuration is successfully applied to the device, you will + notice a new ZeroTier interface that is up and running. This interface + will have the name ``owzt89f498`` (where ``owzt`` is followed by the + last six hexadecimal characters of the ZeroTier **network ID**). .. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png :target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/zerotier-tutorial/device-configuration-2.png :alt: ZeroTier VPN device configuration example 2 -**Congratulations!** You've successfully configured OpenWISP to manage ZeroTier tunnels -on your devices. +**Congratulations!** You've successfully configured OpenWISP to manage +ZeroTier tunnels on your devices. .. seealso:: - You may also want to explore other automated VPN tunnel provisioning options: + You may also want to explore other automated VPN tunnel provisioning + options: - :doc:`Wireguard ` - :doc:`Wireguard over VXLAN ` From efa4532d8598190a77da3adafc1d9729f600820f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 22 Jul 2024 19:22:05 -0400 Subject: [PATCH 41/44] [docs] Added architecture diagram [skip ci] --- .../architecture-v2-openwisp-controller.png | Bin 0 -> 397425 bytes docs/index.rst | 33 +++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 docs/images/architecture-v2-openwisp-controller.png diff --git a/docs/images/architecture-v2-openwisp-controller.png b/docs/images/architecture-v2-openwisp-controller.png new file mode 100644 index 0000000000000000000000000000000000000000..c1059c64a81f00ab8ae20304ea59834d2fd8ac83 GIT binary patch literal 397425 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 A5dZ)H literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index a2441654f..dea5d9c93 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,35 @@ Controller ========== -**Source code**: `github.com/openwisp/openwisp-controller -`_. +.. seealso:: -OpenWISP Controller is responsible of of managing the core resources of -the network and allows automating several aspects like adoption, -provisioning, VPN tunnel configuration, generation of X509 certificates, -subnet and IP address allocation and more. + **Source code**: `github.com/openwisp/openwisp-controller + `_. + +OpenWISP Controller is responsible of of managing the core resources of the network and +allows automating several aspects like adoption, provisioning, VPN tunnel configuration, +generation of X509 certificates, subnet and IP address allocation and more. For a full introduction please refer to :doc:`user/intro`. +The following diagram illustrates the role of the Controller module within the OpenWISP +architecture. + +.. figure:: images/architecture-v2-openwisp-controller.png + :target: ../_images/architecture-v2-openwisp-controller.png + :align: center + :alt: OpenWISP Architecture: Controller module + + **OpenWISP Architecture: highlighted controller 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: Controller Module Usage Docs :maxdepth: 1 user/intro.rst @@ -32,7 +49,7 @@ For a full introduction please refer to :doc:`user/intro`. user/settings.rst .. toctree:: - :caption: Developer Docs + :caption: Controller Module Developer Docs :maxdepth: 2 Developer Docs Index From 0ed9ff44c625ff83132495ae3a9fdc4f24b85d44 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 27 Jul 2024 14:50:27 -0400 Subject: [PATCH 42/44] [docs] Reformatted index [skip ci] --- docs/index.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index dea5d9c93..40d4e9301 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,14 +6,15 @@ Controller **Source code**: `github.com/openwisp/openwisp-controller `_. -OpenWISP Controller is responsible of of managing the core resources of the network and -allows automating several aspects like adoption, provisioning, VPN tunnel configuration, -generation of X509 certificates, subnet and IP address allocation and more. +OpenWISP Controller is responsible of of managing the core resources of +the network and allows automating several aspects like adoption, +provisioning, VPN tunnel configuration, generation of X509 certificates, +subnet and IP address allocation and more. For a full introduction please refer to :doc:`user/intro`. -The following diagram illustrates the role of the Controller module within the OpenWISP -architecture. +The following diagram illustrates the role of the Controller module within +the OpenWISP architecture. .. figure:: images/architecture-v2-openwisp-controller.png :target: ../_images/architecture-v2-openwisp-controller.png @@ -24,7 +25,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 3674bd9a571cacb7e4d1c1a9867fba589fbe95a1 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 1 Aug 2024 18:56:23 -0400 Subject: [PATCH 43/44] [docs] Reformatted CONTRIBUTING.rst --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9f082dd1d..e1b58e460 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,2 +1,2 @@ Please refer to the `OpenWISP Contribution Guidelines -`_. \ No newline at end of file +`_. From d1800144c391669373900a6c99e94a66d847905d Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sat, 3 Aug 2024 10:51:19 -0400 Subject: [PATCH 44/44] [qa] Don't skip README in docstrfmt --- README.rst | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e4d85e599..c353faf2a 100644 --- a/README.rst +++ b/README.rst @@ -82,7 +82,8 @@ Documentation ------------- - `Developer documentation `_ -- `User documentation `_ +- `User documentation + `_ Contributing ------------ diff --git a/pyproject.toml b/pyproject.toml index eb261c779..c1f5a1ad4 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"]