├── .env ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── Makefile ├── README.rst ├── build.py ├── customization └── configuration │ └── django │ ├── __init__.py │ └── custom_django_settings.example.py ├── deploy └── auto-install.sh ├── docker-compose.yml ├── docs ├── developer │ └── instructions.rst ├── images │ ├── architecture-v2-docker-openwisp.png │ ├── architecture.jpg │ ├── auto-install.png │ └── portainer-docker-list.png ├── index.rst ├── partials │ ├── developer-docs.rst │ └── updating-host-file.rst └── user │ ├── architecture.rst │ ├── customization.rst │ ├── faq.rst │ ├── quickstart.rst │ └── settings.rst ├── images ├── common │ ├── init_command.sh │ ├── manage.py │ ├── openwisp │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── celery.py │ │ ├── routing.py │ │ ├── settings.py │ │ ├── tasks.py │ │ ├── utils.py │ │ └── wsgi.py │ ├── services.py │ ├── utils.py │ ├── utils.sh │ └── uwsgi.conf.ini ├── openwisp_api │ ├── Dockerfile │ ├── module_settings.py │ └── urls.py ├── openwisp_base │ ├── Dockerfile │ └── requirements.txt ├── openwisp_dashboard │ ├── Dockerfile │ ├── load_init_data.py │ ├── module_settings.py │ ├── openvpn.json │ └── urls.py ├── openwisp_freeradius │ ├── Dockerfile │ └── raddb │ │ ├── dictionary │ │ ├── mods-enabled │ │ ├── rest │ │ ├── sql │ │ └── sqlcounter │ │ ├── radiusd.conf │ │ └── sites-enabled │ │ └── default ├── openwisp_nfs │ ├── Dockerfile │ └── init_command.sh ├── openwisp_nginx │ ├── Dockerfile │ ├── get_domain.py │ ├── nginx.template.conf │ ├── openwisp.internal.template.conf │ ├── openwisp.ssl.80.template.conf │ ├── openwisp.ssl.template.conf │ ├── openwisp.template.conf │ └── requirements.txt ├── openwisp_openvpn │ ├── Dockerfile │ ├── openvpn.sh │ ├── revokelist.sh │ ├── send-topology.sh │ └── supervisord.conf ├── openwisp_postfix │ ├── Dockerfile │ └── rsyslog.conf └── openwisp_websocket │ ├── Dockerfile │ ├── daphne.conf │ ├── module_settings.py │ └── urls.py ├── qa-format ├── requirements-test.txt ├── run-qa-checks ├── setup.cfg └── tests ├── config.json ├── data.py ├── runtests.py ├── static └── network-graph.json └── utils.py /.env: -------------------------------------------------------------------------------- 1 | # These are just basic options, more settings are available in the 2 | # documentation: https://github.com/openwisp/docker-openwisp/blob/master/docs/ENV.md 3 | 4 | # Essential 5 | DASHBOARD_DOMAIN=dashboard.openwisp.org 6 | API_DOMAIN=api.openwisp.org 7 | # SSH Credentials Configurations 8 | SSH_PRIVATE_KEY_PATH=/home/openwisp/.ssh/id_ed25519 9 | SSH_PUBLIC_KEY_PATH=/home/openwisp/.ssh/id_ed25519.pub 10 | VPN_DOMAIN=openvpn.openwisp.org 11 | EMAIL_DJANGO_DEFAULT=example@example.org 12 | DB_USER=admin 13 | DB_PASS=admin 14 | INFLUXDB_USER=admin 15 | INFLUXDB_PASS=admin 16 | # Security 17 | DJANGO_SECRET_KEY=default_secret_key 18 | # Enable Modules 19 | USE_OPENWISP_RADIUS=True 20 | USE_OPENWISP_TOPOLOGY=True 21 | USE_OPENWISP_FIRMWARE=True 22 | USE_OPENWISP_MONITORING=True 23 | # uWSGI 24 | UWSGI_PROCESSES=2 25 | UWSGI_THREADS=2 26 | UWSGI_LISTEN=100 27 | # Additional 28 | SSL_CERT_MODE=SelfSigned 29 | TZ=Asia/Kolkata 30 | CERT_ADMIN_EMAIL=example@example.org 31 | DJANGO_LANGUAGE_CODE=en-gb 32 | DB_NAME=openwisp 33 | INFLUXDB_NAME=openwisp 34 | OPENWISP_GEOCODING_CHECK=True 35 | # X509 default CA & Certs Information 36 | X509_NAME_CA=default 37 | X509_NAME_CERT=default 38 | X509_COUNTRY_CODE=IN 39 | X509_STATE=Delhi 40 | X509_CITY=New Delhi 41 | X509_ORGANIZATION_NAME=OpenWISP 42 | X509_ORGANIZATION_UNIT_NAME=OpenWISP 43 | X509_EMAIL=certificate@example.com 44 | X509_COMMON_NAME=OpenWISP 45 | # VPN 46 | VPN_NAME=default 47 | VPN_CLIENT_NAME=default-management-vpn 48 | # Developer 49 | DEBUG_MODE=False 50 | DJANGO_LOG_LEVEL=INFO 51 | # Celery workers 52 | USE_OPENWISP_CELERY_TASK_ROUTES_DEFAULTS=True 53 | OPENWISP_CELERY_COMMAND_FLAGS=--concurrency=1 54 | USE_OPENWISP_CELERY_NETWORK=True 55 | OPENWISP_CELERY_NETWORK_COMMAND_FLAGS=--concurrency=1 56 | USE_OPENWISP_CELERY_MONITORING=True 57 | OPENWISP_CELERY_MONITORING_COMMAND_FLAGS=--concurrency=1 58 | OPENWISP_CELERY_MONITORING_CHECKS_COMMAND_FLAGS=--concurrency=1 59 | USE_OPENWISP_CELERY_FIRMWARE=True 60 | OPENWISP_CELERY_FIRMWARE_COMMAND_FLAGS=--concurrency=1 61 | # Metric collection 62 | METRIC_COLLECTION=True 63 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ["https://openwisp.org/sponsorship/"] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Open a bug report 4 | title: "[bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of the bug or unexpected behavior. 12 | 13 | **Steps To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **System Informatioon:** 27 | - OS: [e.g. Ubuntu 24.04 LTS] 28 | - Docker version: [e.g. Docker version 27.0.3, build 7d4bcd8] 29 | - Browser and Browser Version (if applicable): [e.g. Chromium v126.0.6478.126] 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Please use the Discussion Forum to ask questions 4 | title: "[question] " 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please use the [Discussion Forum](https://github.com/orgs/openwisp/discussions) to ask questions. 11 | 12 | We will take care of moving the discussion to a more relevant repository if needed. 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directories: 5 | - "**/*" 6 | schedule: 7 | interval: "monthly" 8 | commit-message: 9 | prefix: "[deps] " 10 | - package-ecosystem: "docker" 11 | directories: 12 | - "**/*" 13 | schedule: 14 | interval: "monthly" 15 | commit-message: 16 | prefix: "[deps] " 17 | - package-ecosystem: "github-actions" # Check for GitHub Actions updates 18 | directory: "/" # The root directory where the Ansible role is located 19 | schedule: 20 | interval: "monthly" # Check for updates weekly 21 | commit-message: 22 | prefix: "[ci] " 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Checklist 2 | 3 | - [ ] I have read the [OpenWISP Contributing Guidelines](http://openwisp.io/docs/developer/contributing.html). 4 | - [ ] I have manually tested the changes proposed in this pull request. 5 | - [ ] I have written new test cases for new code and/or updated existing tests for changes to existing code. 6 | - [ ] I have updated the documentation. 7 | 8 | ## Reference to Existing Issue 9 | 10 | Closes #. 11 | 12 | Please [open a new issue](https://github.com/openwisp/docker-openwisp/issues/new/choose) if there isn't an existing issue yet. 13 | 14 | ## Description of Changes 15 | 16 | Please describe these changes. 17 | 18 | ## Screenshot 19 | 20 | Please include any relevant screenshots. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Merge Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | name: CI Build 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - name: Git Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.sha }} 21 | 22 | - name: Setup testing environment 23 | id: deps 24 | run: | 25 | sudo curl -sL -o /bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64" 26 | sudo chmod +x /bin/hadolint 27 | echo "127.0.0.1 dashboard.openwisp.org api.openwisp.org" | sudo tee -a /etc/hosts 28 | # disable metric collection during builds 29 | sed -i 's/METRIC_COLLECTION=True/METRIC_COLLECTION=False/' .env 30 | sudo pip3 install -r requirements-test.txt 31 | 32 | - name: QA checks 33 | run: ./run-qa-checks 34 | 35 | - name: Use the auto-install script to start containers with edge images 36 | id: auto_install_edge 37 | if: ${{ !cancelled() && steps.deps.conclusion == 'success' }} 38 | run: | 39 | (sudo -E ./deploy/auto-install.sh <> $GITHUB_ENV 61 | echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV 62 | 63 | - name: Use auto-install script to upgrade containers to latest version 64 | id: auto_install_upgrade 65 | if: ${{ !cancelled() && steps.set_git_branch.conclusion == 'success' }} 66 | # Do not remove the blank lines from the input. 67 | run: | 68 | (GIT_BRANCH="${GIT_BRANCH}" SKIP_PULL=true sudo -E ./deploy/auto-install.sh --upgrade <`_. 33 | 34 | Changes 35 | ~~~~~~~ 36 | 37 | Dependencies 38 | ++++++++++++ 39 | 40 | - Upgraded to OpenWISP Users 1.1.x (see `changelog 41 | `__). 42 | - Upgraded to OpenWISP Controller 1.1.x (see `changelog 43 | `__). 44 | - Upgraded to OpenWISP Monitoring 1.1.x (see `changelog 45 | `__). 46 | - Upgraded to OpenWISP Network Topology 1.1.x (see `changelog 47 | `__). 48 | - Upgraded to OpenWISP Firmware Upgrader 1.1.x (see `changelog 49 | `__). 50 | - Upgraded to OpenWISP RADIUS 1.1.x (see `changelog 51 | `__). 52 | - Updated auto-install script to support Debian 12. 53 | - Updated auto-install script to support Ubuntu 22.04. 54 | - Updated base image of ``openwisp/openwisp-nginx`` to 55 | ``nginx:1.27.2-alpine``. 56 | - Updated base image of ``openwisp/openwisp-freeradius`` to 57 | ``freeradius/freeradius-server:3.2.6-alpine``. 58 | - Updated base image of ``openwisp/openwisp-postfix`` to ``alpine:3.20``. 59 | - Updated base image of ``openwisp/openwisp-openvpn`` to 60 | ``kylemanna/openvpn:2.4``. 61 | - Updated base image of ``openwisp/openwisp-dashboard``, 62 | ``openwisp/openwisp-api``, and ``openwisp/openwisp-websocket`` to 63 | ``python:3.10.0-slim-buster``. 64 | 65 | Backward Incompatible Changes 66 | +++++++++++++++++++++++++++++ 67 | 68 | - Merged the OpenWISP RADIUS container into the dashboard and API. 69 | - The ``CRON_DELETE_OLD_RADIUSBATCH_USERS`` variable now expects the 70 | number of days instead of months. 71 | - Removed ``DJANGO_FREERADIUS_ALLOWED_HOSTS``; use 72 | ``OPENWISP_RADIUS_ALLOWED_HOSTS`` instead. 73 | - Renamed ``CRON_DELETE_OLD_USERS`` to 74 | ``CRON_DELETE_OLD_RADIUSBATCH_USERS``. 75 | 76 | Other Changes 77 | +++++++++++++ 78 | 79 | - Changed cron to update OpenVPN revoke list daily at midnight. 80 | - Added admin URLs to the API container. 81 | - Migrated to Docker Compose v2. 82 | - Geocoding checks are now performed only in the dashboard container. 83 | - Removed ``sudo`` capabilities for containers. 84 | - Main processes no longer run as ``root``. 85 | - Switched the default email backend to ``django-celery-email``. 86 | - Enabled ``django.contrib.humanize`` in installed apps. 87 | - Enabled gzip compression for HTTP responses. 88 | - Disabled nginx ``server_tokens`` for improved security. 89 | 90 | Bugfixes 91 | ~~~~~~~~ 92 | 93 | - Fixed OpenVPN cron script to download configuration at the correct path. 94 | - Fixed project configuration issues in the OpenWISP RADIUS module. 95 | - Fixed monitoring charts not loading on the device's change page. 96 | - Fixed network topology graph stuck at loading. 97 | - Fixed bugs in the auto-install script. 98 | - Fixed missing directory for firmware private storage. 99 | - Fixed duplicate MIME types in nginx gzip configuration. 100 | - Resolved ``OSerror`` in uWSGI. 101 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Please refer to the `OpenWISP Contributing Guidelines 2 | `_. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, OpenWISP 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Find documentation in README.md under 2 | # the heading "Makefile Options". 3 | 4 | OPENWISP_VERSION = 24.11.1 5 | SHELL := /bin/bash 6 | .SILENT: clean pull start stop 7 | 8 | default: compose-build 9 | 10 | USER = registry.gitlab.com/openwisp/docker-openwisp 11 | TAG = edge 12 | SKIP_PULL ?= false 13 | SKIP_BUILD ?= false 14 | SKIP_TESTS ?= false 15 | 16 | # Pull 17 | pull: 18 | printf '\e[1;34m%-6s\e[m\n' "Downloading OpenWISP images..." 19 | for image in 'openwisp-base' 'openwisp-nfs' 'openwisp-api' 'openwisp-dashboard' \ 20 | 'openwisp-freeradius' 'openwisp-nginx' 'openwisp-openvpn' 'openwisp-postfix' \ 21 | 'openwisp-websocket' ; do \ 22 | docker pull --quiet $(USER)/$${image}:$(TAG); \ 23 | docker tag $(USER)/$${image}:$(TAG) openwisp/$${image}:latest; \ 24 | done 25 | 26 | # Build 27 | python-build: build.py 28 | python build.py change-secret-key 29 | 30 | base-build: 31 | BUILD_ARGS_FILE=$$(cat .build.env 2>/dev/null); \ 32 | for build_arg in $$BUILD_ARGS_FILE; do \ 33 | BUILD_ARGS+=" --build-arg $$build_arg"; \ 34 | done; \ 35 | docker build --tag openwisp/openwisp-base:intermedia-system \ 36 | --file ./images/openwisp_base/Dockerfile \ 37 | --target SYSTEM ./images/; \ 38 | docker build --tag openwisp/openwisp-base:intermedia-python \ 39 | --file ./images/openwisp_base/Dockerfile \ 40 | --target PYTHON ./images/ \ 41 | $$BUILD_ARGS; \ 42 | docker build --tag openwisp/openwisp-base:latest \ 43 | --file ./images/openwisp_base/Dockerfile ./images/ \ 44 | $$BUILD_ARGS 45 | 46 | nfs-build: 47 | docker build --tag openwisp/openwisp-nfs:latest \ 48 | --file ./images/openwisp_nfs/Dockerfile ./images/ 49 | 50 | compose-build: base-build 51 | docker compose build --parallel 52 | 53 | # Test 54 | runtests: develop-runtests 55 | docker compose stop 56 | 57 | develop-runtests: 58 | docker compose up -d 59 | make develop-pythontests 60 | 61 | develop-pythontests: 62 | python3 tests/runtests.py 63 | 64 | # Development 65 | develop: compose-build 66 | docker compose up -d 67 | docker compose logs -f 68 | 69 | # Clean 70 | clean: 71 | printf '\e[1;34m%-6s\e[m\n' "Removing docker-openwisp..." 72 | docker compose stop &> /dev/null 73 | docker compose down --remove-orphans --volumes --rmi all &> /dev/null 74 | docker compose rm -svf &> /dev/null 75 | docker rmi --force openwisp/openwisp-base:latest \ 76 | openwisp/openwisp-base:intermedia-system \ 77 | openwisp/openwisp-base:intermedia-python \ 78 | openwisp/openwisp-nfs:latest \ 79 | `docker images -f "dangling=true" -q` \ 80 | `docker images | grep openwisp/docker-openwisp | tr -s ' ' | cut -d ' ' -f 3` &> /dev/null 81 | 82 | # Production 83 | start: 84 | if [ "$(SKIP_PULL)" == "false" ]; then \ 85 | make pull; \ 86 | fi 87 | printf '\e[1;34m%-6s\e[m\n' "Starting Services..." 88 | docker --log-level WARNING compose up -d 89 | printf '\e[1;32m%-6s\e[m\n' "Success: OpenWISP should be available at your dashboard domain in 2 minutes." 90 | 91 | stop: 92 | printf '\e[1;31m%-6s\e[m\n' "Stopping OpenWISP services..." 93 | docker --log-level ERROR compose stop 94 | docker --log-level ERROR compose down --remove-orphans 95 | docker compose down --remove-orphans &> /dev/null 96 | 97 | # Publish 98 | publish: 99 | if [[ "$(SKIP_BUILD)" == "false" ]]; then \ 100 | make compose-build nfs-build; \ 101 | fi 102 | if [[ "$(SKIP_TESTS)" == "false" ]]; then \ 103 | make runtests; \ 104 | fi 105 | for image in 'openwisp-base' 'openwisp-nfs' 'openwisp-api' 'openwisp-dashboard' \ 106 | 'openwisp-freeradius' 'openwisp-nginx' 'openwisp-openvpn' 'openwisp-postfix' \ 107 | 'openwisp-websocket' ; do \ 108 | # Docker images built locally are tagged "latest" by default. \ 109 | # This script updates the tag of each built image to a user-defined tag \ 110 | # and pushes the newly tagged image to a Docker registry under the user's namespace. \ 111 | docker tag openwisp/$${image}:latest $(USER)/$${image}:$(TAG); \ 112 | docker push $(USER)/$${image}:$(TAG); \ 113 | if [ "$(TAG)" != "latest" ]; then \ 114 | docker rmi $(USER)/$${image}:$(TAG); \ 115 | fi; \ 116 | done 117 | 118 | release: 119 | make publish TAG=latest SKIP_TESTS=true 120 | make publish TAG=$(OPENWISP_VERSION) SKIP_BUILD=true SKIP_TESTS=true 121 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Docker-OpenWISP 2 | =============== 3 | 4 | .. image:: https://github.com/openwisp/docker-openwisp/workflows/Automation%20Tests/badge.svg 5 | :target: https://github.com/openwisp/docker-openwisp/actions?query=workflow%3A%22Automation+Tests%22 6 | 7 | .. image:: https://img.shields.io/badge/registry-openwisp-blue.svg 8 | :target: https://gitlab.com/openwisp/docker-openwisp/container_registry 9 | 10 | .. image:: https://badges.gitter.im/openwisp/dockerize-openwisp.svg 11 | :target: https://gitter.im/openwisp/dockerize-openwisp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge 12 | 13 | .. image:: https://img.shields.io/badge/support-orange.svg 14 | :target: http://openwisp.org/support.html 15 | 16 | .. image:: https://img.shields.io/github/license/openwisp/docker-openwisp.svg 17 | :target: https://github.com/openwisp/docker-openwisp/blob/master/LICENSE 18 | 19 | This repository contains official docker images of OpenWISP. Designed with 20 | horizontal scaling, easily replicable deployments and user customization 21 | in mind. 22 | 23 | .. image:: https://raw.githubusercontent.com/openwisp/docker-openwisp/master/docs/images/portainer-docker-list.png 24 | :target: https://raw.githubusercontent.com/openwisp/docker-openwisp/master/docs/images/portainer-docker-list.png 25 | 26 | Documentation 27 | ------------- 28 | 29 | - `Usage documentation `_ 30 | - `Developer documentation 31 | `_ 32 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | # This python script will take care of anything 2 | # required during building the images. It is 3 | # used by Makefile. 4 | 5 | import random 6 | import re 7 | import sys 8 | 9 | 10 | def update_env_file(key, value): 11 | # Update the generated secret key 12 | # in the .env file. 13 | 14 | with open(".env", "r") as file_handle: 15 | file_string = file_handle.read() 16 | file_string = re.sub(rf"{key}=.*", rf"{key}={value}", file_string) 17 | if file_string[-1] != "\n": 18 | file_string += "\n" 19 | if f"{key}" not in file_string: 20 | file_string += f"{key}={value}" 21 | with open(".env", "w") as file_handle: 22 | file_handle.write(file_string) 23 | 24 | 25 | def get_secret_key(allow_special_chars=True): 26 | chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789" 27 | if allow_special_chars: 28 | chars += "#^[]-_*%&=+/" 29 | keygen = "".join([random.SystemRandom().choice(chars) for _ in range(50)]) 30 | print(keygen) 31 | return keygen 32 | 33 | 34 | if __name__ == "__main__": 35 | arguments = sys.argv[1:] 36 | if "get-secret-key" in arguments: 37 | get_secret_key() 38 | if "change-secret-key" in arguments: 39 | keygen = get_secret_key() 40 | update_env_file("DJANGO_SECRET_KEY", keygen) 41 | if "default-secret-key" in arguments: 42 | update_env_file("DJANGO_SECRET_KEY", "default_secret_key") 43 | if "change-database-credentials" in arguments: 44 | keygen1 = get_secret_key(allow_special_chars=False) 45 | keygen2 = get_secret_key() 46 | update_env_file("DB_USER", keygen1) 47 | update_env_file("DB_PASS", keygen2) 48 | -------------------------------------------------------------------------------- /customization/configuration/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/docker-openwisp/c931b6f07a2bca2bff758378959365718acf992a/customization/configuration/django/__init__.py -------------------------------------------------------------------------------- /customization/configuration/django/custom_django_settings.example.py: -------------------------------------------------------------------------------- 1 | # RENAME THIS FILE TO custom_django_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS 2 | # BUT DO NOT COMMIT 3 | 4 | # EMAIL_HOST = 5 | # EMAIL_HOST_USER = 6 | # EMAIL_HOST_PASSWORD = 7 | # EMAIL_PORT = 8 | # EMAIL_USE_TLS = 9 | # EMAIL_TIMEOUT = 10 | -------------------------------------------------------------------------------- /deploy/auto-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export DEBIAN_FRONTEND=noninteractive 4 | export INSTALL_PATH=/opt/openwisp/docker-openwisp 5 | export LOG_FILE=/opt/openwisp/autoinstall.log 6 | export ENV_USER=/opt/openwisp/config.env 7 | export ENV_BACKUP=/opt/openwisp/backup.env 8 | export GIT_PATH=${GIT_PATH:-https://github.com/openwisp/docker-openwisp.git} 9 | 10 | # Terminal colors 11 | export RED='\033[1;31m' 12 | export GRN='\033[1;32m' 13 | export YLW='\033[1;33m' 14 | export BLU='\033[1;34m' 15 | export NON='\033[0m' 16 | 17 | start_step() { printf '\e[1;34m%-70s\e[m' "$1" && echo "$1" &>>$LOG_FILE; } 18 | report_ok() { echo -e ${GRN}" done"${NON}; } 19 | report_error() { echo -e ${RED}" error"${NON}; } 20 | get_env() { grep "^$1" "$2" | cut -d'=' -f 2-50; } 21 | set_env() { 22 | line=$(grep -n "^$1=" $INSTALL_PATH/.env) 23 | if [ -z "$line" ]; then 24 | echo "$1=$2" >>$INSTALL_PATH/.env 25 | else 26 | line_number=$(echo $line | cut -f1 -d:) 27 | eval $(echo "awk -i inplace 'NR=="${line_number}" {\$0=\"${1}=${2}\"}1' $INSTALL_PATH/.env") 28 | fi 29 | } 30 | 31 | check_status() { 32 | if [ $1 -eq 0 ]; then 33 | report_ok 34 | else 35 | error_msg "$2" 36 | fi 37 | } 38 | 39 | error_msg() { 40 | report_error 41 | echo -e ${RED}${1}${NON} 42 | echo -e ${RED}"Check logs at $LOG_FILE"${NON} 43 | exit 1 44 | } 45 | 46 | error_msg_with_continue() { 47 | report_error 48 | echo -ne ${RED}${1}${NON} 49 | read reply 50 | if [[ ! $reply =~ ^[Yy]$ ]]; then 51 | exit 1 52 | fi 53 | } 54 | 55 | apt_dependenices_setup() { 56 | start_step "Setting up dependencies..." 57 | apt --yes install python3 python3-pip git python3-dev gawk libffi-dev libssl-dev gcc make curl jq &>>$LOG_FILE 58 | check_status $? "Python dependencies installation failed." 59 | } 60 | 61 | get_version_from_user() { 62 | echo -ne ${GRN}"OpenWISP Version (leave blank for latest): "${NON} 63 | read openwisp_version 64 | if [[ -z "$openwisp_version" ]]; then 65 | openwisp_version=$(curl -L --silent https://api.github.com/repos/openwisp/docker-openwisp/releases/latest | jq -r .tag_name) 66 | fi 67 | } 68 | 69 | setup_docker() { 70 | start_step "Setting up docker..." 71 | docker info &>/dev/null 72 | if [ $? -eq 0 ]; then 73 | report_ok 74 | else 75 | curl -fsSL 'https://get.docker.com' -o '/opt/openwisp/get-docker.sh' &>>$LOG_FILE 76 | sh '/opt/openwisp/get-docker.sh' &>>$LOG_FILE 77 | docker info &>/dev/null 78 | check_status $? "Docker installation failed." 79 | fi 80 | } 81 | 82 | download_docker_openwisp() { 83 | local openwisp_version="$1" 84 | start_step "Downloading docker-openwisp..." 85 | if [[ -f $INSTALL_PATH/.env ]]; then 86 | mv $INSTALL_PATH/.env $ENV_BACKUP &>>$LOG_FILE 87 | rm -rf $INSTALL_PATH &>>$LOG_FILE 88 | fi 89 | if [ -z "$GIT_BRANCH" ]; then 90 | if [[ "$openwisp_version" == "edge" ]]; then 91 | GIT_BRANCH="master" 92 | else 93 | GIT_BRANCH="$openwisp_version" 94 | fi 95 | fi 96 | 97 | git clone $GIT_PATH $INSTALL_PATH --depth 1 --branch $GIT_BRANCH &>>$LOG_FILE 98 | } 99 | 100 | setup_docker_openwisp() { 101 | echo -e ${GRN}"\nOpenWISP Configuration:"${NON} 102 | get_version_from_user 103 | echo -ne ${GRN}"Do you have .env file? Enter filepath (leave blank for ad-hoc configuration): "${NON} 104 | read env_path 105 | if [[ ! -f "$env_path" ]]; then 106 | # Dashboard Domain 107 | echo -ne ${GRN}"(1/5) Enter dashboard domain: "${NON} 108 | read dashboard_domain 109 | domain=$(echo "$dashboard_domain" | cut -f2- -d'.') 110 | # API Domain 111 | echo -ne ${GRN}"(2/5) Enter API domain (blank for api.${domain}): "${NON} 112 | read api_domain 113 | # VPN domain 114 | echo -ne ${GRN}"(3/5) Enter OpenVPN domain (blank for openvpn.${domain}, N to disable module): "${NON} 115 | read vpn_domain 116 | # Site manager email 117 | echo -ne ${GRN}"(4/5) Site manager email: "${NON} 118 | read django_default_email 119 | # SSL Configuration 120 | echo -ne ${GRN}"(5/5) Enter letsencrypt email (leave blank for self-signed certificate): "${NON} 121 | read letsencrypt_email 122 | else 123 | cp $env_path $ENV_USER &>>$LOG_FILE 124 | fi 125 | echo "" 126 | 127 | download_docker_openwisp "$openwisp_version" 128 | 129 | cd $INSTALL_PATH &>>$LOG_FILE 130 | check_status $? "docker-openwisp download failed." 131 | echo $openwisp_version >$INSTALL_PATH/VERSION 132 | 133 | if [[ ! -f "$env_path" ]]; then 134 | # Dashboard Domain 135 | set_env "DASHBOARD_DOMAIN" "$dashboard_domain" 136 | # API Domain 137 | if [[ -z "$api_domain" ]]; then 138 | set_env "API_DOMAIN" "api.${domain}" 139 | else 140 | set_env "API_DOMAIN" "$api_domain" 141 | fi 142 | # Use Radius 143 | if [[ -z "$USE_OPENWISP_RADIUS" ]]; then 144 | set_env "USE_OPENWISP_RADIUS" "Yes" 145 | else 146 | set_env "USE_OPENWISP_RADIUS" "No" 147 | fi 148 | # VPN domain 149 | if [[ -z "$vpn_domain" ]]; then 150 | set_env "VPN_DOMAIN" "openvpn.${domain}" 151 | elif [[ "${vpn_domain,,}" == "n" ]]; then 152 | set_env "VPN_DOMAIN" "example.com" 153 | else 154 | set_env "VPN_DOMAIN" "$vpn_domain" 155 | fi 156 | # Site manager email 157 | set_env "EMAIL_DJANGO_DEFAULT" "$django_default_email" 158 | # Set random secret values 159 | python3 $INSTALL_PATH/build.py change-secret-key >/dev/null 160 | python3 $INSTALL_PATH/build.py change-database-credentials >/dev/null 161 | # SSL Configuration 162 | set_env "CERT_ADMIN_EMAIL" "$letsencrypt_email" 163 | if [[ -z "$letsencrypt_email" ]]; then 164 | set_env "SSL_CERT_MODE" "SelfSigned" 165 | else 166 | set_env "SSL_CERT_MODE" "Yes" 167 | fi 168 | # Other 169 | hostname=$(echo "$django_default_email" | cut -d @ -f 2) 170 | set_env "POSTFIX_ALLOWED_SENDER_DOMAINS" "$hostname" 171 | set_env "POSTFIX_MYHOSTNAME" "$hostname" 172 | else 173 | mv $ENV_USER $INSTALL_PATH/.env &>>$LOG_FILE 174 | rm -rf $ENV_USER &>>$LOG_FILE 175 | fi 176 | 177 | start_step "Configuring docker-openwisp..." 178 | report_ok 179 | start_step "Starting images docker-openwisp (this will take a while)..." 180 | make start TAG=$(cat $INSTALL_PATH/VERSION) -C $INSTALL_PATH/ &>>$LOG_FILE 181 | check_status $? "Starting openwisp failed." 182 | } 183 | 184 | upgrade_docker_openwisp() { 185 | echo -e ${GRN}"\nOpenWISP Configuration:"${NON} 186 | get_version_from_user 187 | echo "" 188 | 189 | download_docker_openwisp "$openwisp_version" 190 | 191 | cd $INSTALL_PATH &>>$LOG_FILE 192 | check_status $? "docker-openwisp download failed." 193 | echo $openwisp_version >$INSTALL_PATH/VERSION 194 | 195 | start_step "Configuring docker-openwisp..." 196 | for config in $(grep '=' $ENV_BACKUP | cut -f1 -d'='); do 197 | value=$(get_env "$config" "$ENV_BACKUP") 198 | set_env "$config" "$value" 199 | done 200 | report_ok 201 | 202 | start_step "Starting images docker-openwisp (this will take a while)..." 203 | make start TAG=$(cat $INSTALL_PATH/VERSION) -C $INSTALL_PATH/ &>>$LOG_FILE 204 | check_status $? "Starting openwisp failed." 205 | } 206 | 207 | give_information_to_user() { 208 | dashboard_domain=$(get_env "DASHBOARD_DOMAIN" "$INSTALL_PATH/.env") 209 | db_user=$(get_env "DB_USER" "$INSTALL_PATH/.env") 210 | db_pass=$(get_env "DB_PASS" "$INSTALL_PATH/.env") 211 | 212 | echo -e ${GRN}"\nYour setup is ready, your dashboard should be available on https://${dashboard_domain} in 2 minutes.\n" 213 | echo -e "You can login on the dashboard with" 214 | echo -e " username: admin" 215 | echo -e " password: admin" 216 | echo -e "Please remember to change these credentials.\n" 217 | echo -e "Random database user and password generate by the script:" 218 | echo -e " username: ${db_user}" 219 | echo -e " password: ${db_pass}" 220 | echo -e "Please note them, might be helpful for accessing postgresql data in future.\n"${NON} 221 | } 222 | 223 | upgrade_debian() { 224 | apt_dependenices_setup 225 | upgrade_docker_openwisp 226 | dashboard_domain=$(get_env "DASHBOARD_DOMAIN" "$INSTALL_PATH/.env") 227 | echo -e ${GRN}"\nYour upgrade was successfully done." 228 | echo -e "Your dashboard should be available on https://${dashboard_domain} in 2 minutes.\n"${NON} 229 | } 230 | 231 | install_debian() { 232 | apt_dependenices_setup 233 | setup_docker 234 | setup_docker_openwisp 235 | give_information_to_user 236 | } 237 | 238 | init_setup() { 239 | if [[ "$1" == "upgrade" ]]; then 240 | echo -e ${GRN}"Welcome to OpenWISP auto-upgradation script." 241 | echo -e "You are running the upgrade option to change version of" 242 | echo -e "OpenWISP already setup with this script.\n"${NON} 243 | else 244 | echo -e ${GRN}"Welcome to OpenWISP auto-installation script." 245 | echo -e "Please ensure following requirements:" 246 | echo -e " - Fresh instance" 247 | echo -e " - 2GB RAM (Minimum)" 248 | echo -e " - Root privileges" 249 | echo -e " - Supported systems" 250 | echo -e " - Debian: 11 & 12" 251 | echo -e " - Ubuntu 22.04 & 24.04" 252 | echo -e ${YLW}"\nYou can use -u\--upgrade if you are upgrading from an older version.\n"${NON} 253 | fi 254 | 255 | if [ "$EUID" -ne 0 ]; then 256 | echo -e ${RED}"Please run with root privileges."${NON} 257 | exit 1 258 | fi 259 | 260 | mkdir -p /opt/openwisp 261 | echo "" >$LOG_FILE 262 | 263 | start_step "Checking your system capabilities..." 264 | apt update &>>$LOG_FILE 265 | apt -qq --yes install lsb-release &>>$LOG_FILE 266 | system_id=$(lsb_release --id --short) 267 | system_release=$(lsb_release --release --short) 268 | incompatible_message="$system_id $system_release is not supported. Installation might fail, continue anyway? (Y/n): " 269 | 270 | if [[ "$system_id" == "Debian" || "$system_id" == "Ubuntu" ]]; then 271 | case "$system_release" in 272 | 22.04 | 24.04 | 11 | 12) 273 | if [[ "$1" == "upgrade" ]]; then 274 | report_ok && upgrade_debian 275 | else 276 | report_ok && install_debian 277 | fi 278 | ;; 279 | *) 280 | error_msg_with_continue "$incompatible_message" 281 | install_debian 282 | ;; 283 | esac 284 | else 285 | error_msg_with_continue "$incompatible_message" 286 | install_debian 287 | fi 288 | } 289 | 290 | init_help() { 291 | echo -e ${GRN}"Welcome to OpenWISP auto-installation script.\n" 292 | 293 | echo -e "Please ensure following requirements:" 294 | echo -e " - Fresh instance" 295 | echo -e " - 2GB RAM (Minimum)" 296 | echo -e " - Root privileges" 297 | echo -e " - Supported systems" 298 | echo -e " - Debian: 11 & 12" 299 | echo -e " - Ubuntu 22.04 & 24.04\n" 300 | echo -e " -i\--install : (default) Install OpenWISP" 301 | echo -e " -u\--upgrade : Change OpenWISP version already setup with this script" 302 | echo -e " -h\--help : See this help message" 303 | echo -e ${NON} 304 | } 305 | 306 | ## Parse command line arguments 307 | while test $# != 0; do 308 | case "$1" in 309 | -i | --install) action='install' ;; 310 | -u | --upgrade) action='upgrade' ;; 311 | -h | --help) action='help' ;; 312 | *) action='help' ;; 313 | esac 314 | shift 315 | done 316 | 317 | ## Init script 318 | if [[ "$action" == "help" ]]; then 319 | init_help 320 | elif [[ "$action" == "upgrade" ]]; then 321 | init_setup upgrade 322 | else 323 | init_setup 324 | fi 325 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-celery-depends-on: &celery-depends-on 2 | depends_on: 3 | postgres: 4 | condition: service_started 5 | redis: 6 | condition: service_started 7 | dashboard: 8 | condition: service_started 9 | openvpn: 10 | condition: service_healthy 11 | 12 | services: 13 | dashboard: 14 | image: openwisp/openwisp-dashboard:latest 15 | restart: always 16 | build: 17 | context: images 18 | dockerfile: openwisp_dashboard/Dockerfile 19 | args: 20 | DASHBOARD_APP_PORT: 8000 21 | env_file: 22 | - .env 23 | volumes: 24 | - openwisp_static:/opt/openwisp/static 25 | - openwisp_media:/opt/openwisp/media 26 | - openwisp_private_storage:/opt/openwisp/private 27 | - openwisp_ssh:/home/openwisp/.ssh 28 | - influxdb_data:/var/lib/influxdb 29 | - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro 30 | depends_on: 31 | - postgres 32 | - redis 33 | - postfix 34 | - influxdb 35 | 36 | api: 37 | image: openwisp/openwisp-api:latest 38 | restart: always 39 | build: 40 | context: images 41 | dockerfile: openwisp_api/Dockerfile 42 | args: 43 | API_APP_PORT: 8001 44 | env_file: 45 | - .env 46 | volumes: 47 | - influxdb_data:/var/lib/influxdb 48 | - openwisp_media:/opt/openwisp/media 49 | - openwisp_private_storage:/opt/openwisp/private 50 | - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro 51 | depends_on: 52 | - postgres 53 | - redis 54 | - dashboard 55 | 56 | websocket: 57 | image: openwisp/openwisp-websocket:latest 58 | restart: always 59 | build: 60 | context: images 61 | dockerfile: openwisp_websocket/Dockerfile 62 | args: 63 | WEBSOCKET_APP_PORT: 8002 64 | env_file: 65 | - .env 66 | volumes: 67 | - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro 68 | depends_on: 69 | - dashboard 70 | 71 | celery: 72 | image: openwisp/openwisp-dashboard:latest 73 | restart: always 74 | environment: 75 | - MODULE_NAME=celery 76 | volumes: 77 | - openwisp_media:/opt/openwisp/media 78 | - openwisp_private_storage:/opt/openwisp/private 79 | - openwisp_ssh:/home/openwisp/.ssh 80 | - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro 81 | env_file: 82 | - .env 83 | <<: *celery-depends-on 84 | network_mode: "service:openvpn" 85 | 86 | celery_monitoring: 87 | image: openwisp/openwisp-dashboard:latest 88 | restart: always 89 | environment: 90 | - MODULE_NAME=celery_monitoring 91 | volumes: 92 | - openwisp_media:/opt/openwisp/media 93 | - openwisp_private_storage:/opt/openwisp/private 94 | - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro 95 | env_file: 96 | - .env 97 | <<: *celery-depends-on 98 | network_mode: "service:openvpn" 99 | 100 | celerybeat: 101 | image: openwisp/openwisp-dashboard:latest 102 | restart: always 103 | environment: 104 | - MODULE_NAME=celerybeat 105 | env_file: 106 | - .env 107 | volumes: 108 | - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro 109 | depends_on: 110 | - postgres 111 | - redis 112 | - dashboard 113 | 114 | nginx: 115 | image: openwisp/openwisp-nginx:latest 116 | restart: always 117 | build: 118 | context: images 119 | dockerfile: openwisp_nginx/Dockerfile 120 | env_file: 121 | - .env 122 | volumes: 123 | - openwisp_static:/opt/openwisp/public/static:ro 124 | - openwisp_media:/opt/openwisp/public/media:ro 125 | - openwisp_private_storage:/opt/openwisp/public/private:ro 126 | - openwisp_certs:/etc/letsencrypt 127 | - ./customization/theme:/opt/openwisp/public/custom:ro 128 | networks: 129 | default: 130 | aliases: 131 | - dashboard.internal 132 | - api.internal 133 | ports: 134 | - "80:80" 135 | - "443:443" 136 | depends_on: 137 | - dashboard 138 | - api 139 | - websocket 140 | 141 | freeradius: 142 | image: openwisp/openwisp-freeradius:latest 143 | restart: always 144 | build: 145 | context: images 146 | dockerfile: openwisp_freeradius/Dockerfile 147 | env_file: 148 | - .env 149 | ports: 150 | - "1812:1812/udp" 151 | - "1813:1813/udp" 152 | depends_on: 153 | - postgres 154 | - api 155 | - dashboard 156 | 157 | postfix: 158 | image: openwisp/openwisp-postfix:latest 159 | restart: always 160 | build: 161 | context: images 162 | dockerfile: openwisp_postfix/Dockerfile 163 | env_file: 164 | - .env 165 | volumes: 166 | - openwisp_certs:/etc/ssl/mail 167 | 168 | openvpn: 169 | image: openwisp/openwisp-openvpn:latest 170 | restart: always 171 | build: 172 | context: images 173 | dockerfile: openwisp_openvpn/Dockerfile 174 | ports: 175 | - "1194:1194/udp" 176 | env_file: 177 | - .env 178 | depends_on: 179 | - postgres 180 | cap_add: 181 | - NET_ADMIN 182 | devices: 183 | - /dev/net/tun:/dev/net/tun 184 | healthcheck: 185 | test: ["CMD", "pgrep", "-f", "openvpn"] 186 | interval: 30s 187 | timeout: 10s 188 | retries: 30 189 | start_period: 90s 190 | 191 | postgres: 192 | image: postgis/postgis:15-3.4-alpine 193 | restart: always 194 | environment: 195 | - POSTGRES_DB=$DB_NAME 196 | - POSTGRES_USER=$DB_USER 197 | - POSTGRES_PASSWORD=$DB_PASS 198 | - TZ=$TZ 199 | volumes: 200 | - postgres_data:/var/lib/postgresql/data 201 | 202 | influxdb: 203 | image: influxdb:1.8-alpine 204 | restart: always 205 | environment: 206 | - INFLUXDB_DB=$INFLUXDB_NAME 207 | - INFLUXDB_USER=$INFLUXDB_USER 208 | - INFLUXDB_USER_PASSWORD=$INFLUXDB_PASS 209 | volumes: 210 | - influxdb_data:/var/lib/influxdb 211 | 212 | redis: 213 | image: redis:alpine 214 | restart: always 215 | volumes: 216 | - redis_data:/data 217 | 218 | volumes: 219 | influxdb_data: {} 220 | postgres_data: {} 221 | redis_data: {} 222 | openwisp_certs: {} 223 | openwisp_ssh: {} 224 | openwisp_media: {} 225 | openwisp_static: {} 226 | openwisp_private_storage: {} 227 | 228 | networks: 229 | default: 230 | ipam: 231 | config: 232 | - subnet: 172.18.0.0/16 233 | -------------------------------------------------------------------------------- /docs/developer/instructions.rst: -------------------------------------------------------------------------------- 1 | Developer Docs 2 | ============== 3 | 4 | .. include:: ../partials/developer-docs.rst 5 | 6 | .. contents:: **Table of Contents**: 7 | :depth: 2 8 | :local: 9 | 10 | .. include:: ../partials/updating-host-file.rst 11 | 12 | Building and Running Images 13 | --------------------------- 14 | 15 | 1. Install Docker. 16 | 2. In the root directory of the repository, run ``make develop``. Once the 17 | containers are ready, you can test them by accessing the domain names 18 | of the modules. 19 | 20 | .. important:: 21 | 22 | - The default username and password are ``admin``. 23 | - The default domains are ``dashboard.openwisp.org`` and 24 | ``api.openwisp.org``. 25 | - You will need to repeat step 2 each time you make changes and want 26 | to rebuild the images. 27 | - If you want to perform actions such as cleaning everything produced 28 | by ``docker-openwisp``, please refer to the :ref:`makefile options 29 | `. 30 | 31 | Running Tests 32 | ------------- 33 | 34 | You can run tests using either ``geckodriver`` (Firefox) or 35 | ``chromedriver`` (Chromium). 36 | 37 | **Chromium is preferred as it also checks for console log errors.** 38 | 39 | Using Chromedriver 40 | ~~~~~~~~~~~~~~~~~~ 41 | 42 | Install WebDriver for Chromium for your browser version from 43 | https://chromedriver.chromium.org/home and extract ``chromedriver`` to one 44 | of directories from your ``$PATH`` (example: ``~/.local/bin/``). 45 | 46 | Using Geckodriver 47 | ~~~~~~~~~~~~~~~~~ 48 | 49 | Install Geckodriver for Firefox for your browser version from 50 | https://github.com/mozilla/geckodriver/releases and extract 51 | ``geckodriver`` to one of directories from your ``$PATH`` (example: 52 | ``~/.local/bin/``). 53 | 54 | Finish Setup and Run Tests 55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | 1. Install test requirements: 58 | 59 | .. code-block:: bash 60 | 61 | python3 -m pip install -r requirements-test.txt 62 | 63 | 2. (Optional) Modify configuration options in ``tests/config.json``: 64 | 65 | .. code-block:: yaml 66 | 67 | driver: Name of the driver to use for tests, "chromium" or "firefox" 68 | logs: Print container logs if an error occurs 69 | logs_file: Location of the log file for saving logs generated during tests 70 | headless: Run Selenium Chrome driver in headless mode 71 | load_init_data: Flag for running tests/data.py, only needs to be done once after database creation 72 | app_url: URL to reach the admin dashboard 73 | username: Username for logging into the admin dashboard 74 | password: Password for logging into the admin dashboard 75 | services_max_retries: Maximum number of retries to check if services are running 76 | services_delay_retries: Delay time (in seconds) for each retry when checking if services are running 77 | 78 | 3. Run tests with: 79 | 80 | .. code-block:: bash 81 | 82 | make runtests 83 | 84 | 4. To run a single test suite, use the following command: 85 | 86 | .. code-block:: bash 87 | 88 | python3 tests/runtests.py . 89 | 90 | Run Quality Assurance Checks 91 | ---------------------------- 92 | 93 | We use `shfmt `__ to format shell 94 | scripts and `hadolint `__ to 95 | lint Dockerfiles. 96 | 97 | To format all files, run: 98 | 99 | .. code-block:: bash 100 | 101 | ./qa-format 102 | 103 | To run quality assurance checks, use the ``run-qa-checks`` script: 104 | 105 | .. code-block:: bash 106 | 107 | # Run QA checks before committing code 108 | ./run-qa-checks 109 | 110 | .. _docker_make_options: 111 | 112 | Makefile Options 113 | ---------------- 114 | 115 | Most commonly used: 116 | 117 | - ``make start [USER=docker-username] [TAG=image-tag]``: Start OpenWISP 118 | containers on your server. 119 | - ``make pull [USER=docker-username] [TAG=image-tag]``: Pull images from 120 | the registry. 121 | - ``make stop``: Stop OpenWISP containers on your server. 122 | - ``make develop``: Bundle all the commands required to build the images 123 | and run containers. 124 | - ``make runtests``: Start containers and run test cases to ensure all 125 | services are working. It stops containers after the test suite passes. 126 | - ``make clean``: Aggressively purge all containers, images, volumes, and 127 | networks related to ``docker-openwisp``. 128 | 129 | Other options: 130 | 131 | - ``make publish [USER=docker-username] [TAG=image-tag]``: Build, test, 132 | and publish images. 133 | - ``make python-build``: Generate a random Django secret and set it in the 134 | ``.env`` file. 135 | - ``make nfs-build``: Build the OpenWISP NFS server image. 136 | - ``make base-build``: Build the OpenWISP base image. The base image is 137 | used in other OpenWISP images. 138 | - ``make compose-build``: (default) Build OpenWISP images for development. 139 | - ``make develop-runtests``: Similar to ``runtests``, but it doesn't stop 140 | the containers after running the tests, which may be desired for 141 | debugging and analyzing failing container logs. 142 | - ``make develop-pythontests``: Similar to ``develop-runtests``, but it 143 | requires containers to be already running. 144 | -------------------------------------------------------------------------------- /docs/images/architecture-v2-docker-openwisp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/docker-openwisp/c931b6f07a2bca2bff758378959365718acf992a/docs/images/architecture-v2-docker-openwisp.png -------------------------------------------------------------------------------- /docs/images/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/docker-openwisp/c931b6f07a2bca2bff758378959365718acf992a/docs/images/architecture.jpg -------------------------------------------------------------------------------- /docs/images/auto-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/docker-openwisp/c931b6f07a2bca2bff758378959365718acf992a/docs/images/auto-install.png -------------------------------------------------------------------------------- /docs/images/portainer-docker-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/docker-openwisp/c931b6f07a2bca2bff758378959365718acf992a/docs/images/portainer-docker-list.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Docker OpenWISP 2 | =============== 3 | 4 | .. seealso:: 5 | 6 | **Source code**: `github.com/openwisp/docker-openwisp 7 | `_. 8 | 9 | Docker-OpenWISP makes it possible to set up isolated and reproducible 10 | OpenWISP environments, simplifying the deployment and scaling process. 11 | 12 | The following diagram illustrates the role of Docker OpenWISP within the 13 | OpenWISP architecture. 14 | 15 | .. figure:: images/architecture-v2-docker-openwisp.png 16 | :target: ../_images/architecture-v2-docker-openwisp.png 17 | :align: center 18 | :alt: OpenWISP Architecture: Docker OpenWISP 19 | 20 | **OpenWISP Architecture: highlighted Docker OpenWISP** 21 | 22 | .. important:: 23 | 24 | For an enhanced viewing experience, open the image above in a new 25 | browser tab. 26 | 27 | Refer to :doc:`/general/architecture` for more information. 28 | 29 | .. toctree:: 30 | :caption: Docker OpenWISP Usage Docs 31 | :maxdepth: 1 32 | 33 | ./user/quickstart.rst 34 | ./user/architecture.rst 35 | ./user/settings.rst 36 | ./user/customization.rst 37 | ./user/faq.rst 38 | 39 | .. toctree:: 40 | :caption: Docker OpenWISP Developer Docs 41 | :maxdepth: 2 42 | 43 | ./developer/instructions.rst 44 | -------------------------------------------------------------------------------- /docs/partials/developer-docs.rst: -------------------------------------------------------------------------------- 1 | .. note:: 2 | 3 | This page is for developers who want to customize or extend OpenWISP 4 | Controller, whether for bug fixes, new features, or contributions. 5 | 6 | For user guides and general information, please see: 7 | 8 | - :doc:`General OpenWISP Quickstart ` 9 | - :doc:`Docker OpenWISP User Docs ` 10 | -------------------------------------------------------------------------------- /docs/partials/updating-host-file.rst: -------------------------------------------------------------------------------- 1 | .. important:: 2 | 3 | The Docker OpenWISP installation responds only to the `fully qualified 4 | domain names (FQDN) 5 | `_ defined 6 | in the :ref:`configuration `. If you are 7 | deploying locally (for testing), you need to update the ``/etc/hosts`` 8 | file on your machine to resolve the configured domains to localhost. 9 | 10 | For example, the following command will update the ``/etc/hosts`` file 11 | to resolve the domains used in the default configurations: 12 | 13 | .. code-block:: bash 14 | 15 | echo "127.0.0.1 dashboard.openwisp.org api.openwisp.org openvpn.openwisp.org" | \ 16 | sudo tee -a /etc/hosts 17 | -------------------------------------------------------------------------------- /docs/user/architecture.rst: -------------------------------------------------------------------------------- 1 | Architecture 2 | ============ 3 | 4 | A typical OpenWISP installation is made of multiple components (e.g. 5 | application servers, background workers, web servers, database, messaging 6 | queue, VPN server, etc. ) that have different scaling requirements. 7 | 8 | The aim of Docker OpenWISP is to allow deploying OpenWISP in cloud based 9 | environments which allow potentially infinite horizontal scaling. That is 10 | the reason for which there are different docker images shipped in this 11 | repository. 12 | 13 | .. figure:: https://raw.githubusercontent.com/openwisp/docker-openwisp/master/docs/images/architecture.jpg 14 | :target: https://raw.githubusercontent.com/openwisp/docker-openwisp/master/docs/images/architecture.jpg 15 | :alt: Architecture 16 | 17 | Architecture 18 | 19 | - **openwisp-dashboard**: Your OpenWISP device administration dashboard. 20 | - **openwisp-api**: HTTP API from various OpenWISP modules which can be 21 | scaled simply by having multiple API containers as per requirement. 22 | - **openwisp-websocket**: Dedicated container for handling websocket 23 | requests, e.g. for updating location of mobile network devices. 24 | - **openwisp-celery**: Runs all the background tasks for OpenWISP, e.g. 25 | updating configurations of your device. 26 | - **openwisp-celery-monitoring**: Runs background tasks that perform 27 | active monitoring checks, e.g. ping checks and configuration checks. It 28 | also executes task for writing monitoring data to the timeseries DB. 29 | - **openwisp-celerybeat**: Runs periodic background tasks. e.g. revoking 30 | all the expired certificates. 31 | - **openwisp-nginx**: Internet facing container that facilitates all the 32 | HTTP and Websocket communication between the outside world and the 33 | service containers. 34 | - **openwisp-freeradius**: Freeradius container for OpenWISP. 35 | - **openwisp-openvpn**: OpenVPN container for out-of-the-box management 36 | VPN. 37 | - **openwisp-postfix**: Mail server for sending mails to MTA. 38 | - **openwisp-nfs**: NFS server that allows shared storage between 39 | different machines. It does not run in single server machines but 40 | provided for K8s setup. 41 | - **openwisp-base**: It is the base image which does not run on your 42 | server, but openwisp-api & openwisp-dashboard use it as a base. 43 | - **Redis**: data caching service (required for actions like login). 44 | - **PostgreSQL**: SQL database container for OpenWISP. 45 | -------------------------------------------------------------------------------- /docs/user/customization.rst: -------------------------------------------------------------------------------- 1 | Advanced Customization 2 | ====================== 3 | 4 | This page describes advanced customization options for the OpenWISP Docker 5 | images. 6 | 7 | The table of contents below provides a quick overview of the specific 8 | areas that can be customized. 9 | 10 | .. contents:: 11 | :depth: 1 12 | :local: 13 | 14 | Creating the ``customization`` Directory 15 | ---------------------------------------- 16 | 17 | The following commands will create the directory structure required for 18 | adding customizations. Execute these commands in the same location as the 19 | ``docker-compose.yml`` file. 20 | 21 | .. code-block:: shell 22 | 23 | mkdir -p customization/configuration/django 24 | touch customization/configuration/django/__init__.py 25 | touch customization/configuration/django/custom_django_settings.py 26 | mkdir -p customization/theme 27 | 28 | You can also refer to the `directory structure of Docker OpenWISP 29 | repository 30 | `__ 31 | for an example. 32 | 33 | .. _docker_custom_django_settings: 34 | 35 | Supplying Custom Django Settings 36 | -------------------------------- 37 | 38 | The ``customization/configuration/django`` directory created in the 39 | previous section is mounted at ``/opt/openwisp/openwisp/configuration`` in 40 | the ``dashboard``, ``api``, ``celery``, ``celery_monitoring`` and 41 | ``celerybeat`` containers. 42 | 43 | You can specify additional Django settings (e.g. SMTP configuration) in 44 | the ``customization/configuration/django/custom_django_settings.py`` file. 45 | OpenWISP will include these settings during the startup phase. 46 | 47 | You can also put additional files in 48 | ``customization/configuration/django`` that need to be mounted at 49 | ``/opt/openwisp/openwisp/configuration`` in the containers. 50 | 51 | Supplying Custom CSS and JavaScript Files 52 | ----------------------------------------- 53 | 54 | If you want to use your custom styles, add custom JavaScript you can 55 | follow the following guide. 56 | 57 | 1. Read about the option :ref:`openwisp_admin_theme_links`. Please make 58 | `ensure the value you have enter is a valid JSON 59 | `__ and add the desired JSON in ``.env`` file. 60 | example: 61 | 62 | .. code-block:: shell 63 | 64 | # OPENWISP_ADMIN_THEME_LINKS = [ 65 | # { 66 | # "type": "text/css", 67 | # "href": "/static/custom/css/custom-theme.css", 68 | # "rel": "stylesheet", 69 | # "media": "all", 70 | # }, 71 | # { 72 | # "type": "image/x-icon", 73 | # "href": "/static/custom/bootload.png", 74 | # "rel": "icon", 75 | # }, 76 | # { 77 | # "type": "image/svg+xml", 78 | # "href": "/static/ui/openwisp/images/openwisp-logo-small.svg", 79 | # "rel": "icons", 80 | # }, 81 | # ] 82 | # JSON string of the above configuration: 83 | OPENWISP_ADMIN_THEME_LINKS='[{"type": "text/css", "href": "/static/custom/css/custom-theme.css", "rel": "stylesheet", "media": "all"}, {"type": "image/x-icon", "href": "/static/custom/bootload.png", "rel": "icon"}, {"type": "image/svg+xml", "href": "/static/ui/openwisp/images/openwisp-logo-small.svg", "rel": "icons"}]' 84 | 85 | 2. Create your custom CSS / Javascript file in ``customization/theme`` 86 | directory created in the above section. E.g. 87 | ``customization/theme/static/custom/css/custom-theme.css``. 88 | 3. Start the nginx containers. 89 | 90 | .. note:: 91 | 92 | 1. You can edit the styles / JavaScript files now without restarting 93 | the container, as long as file is in the correct place, it will be 94 | picked. 95 | 2. You can create a ``maintenance.html`` file inside the ``customize`` 96 | directory to have a custom maintenance page for scheduled downtime. 97 | 98 | Supplying Custom uWSGI configuration 99 | ------------------------------------ 100 | 101 | By default, you can only configure :ref:`"processes", "threads" and 102 | "listen" settings of uWSGI using environment variables 103 | `. If you want to configure more uWSGI settings, you can 104 | supply your uWSGI configuration by following these steps: 105 | 106 | 1. Create the uWSGI configuration file in the 107 | ``customization/configuration`` directory. For the sake of this 108 | example, let's assume the filename is ``custom_uwsgi.ini``. 109 | 2. In ``dashboard`` and ``api`` services of ``docker-compose.yml``, add 110 | volumes as following 111 | 112 | .. code-block:: yaml 113 | 114 | services: 115 | dashboard: 116 | ... # other configuration 117 | volumes: 118 | ... # other volumes 119 | - ${PWD}/customization/configuration/custom_uwsgi.ini:/opt/openwisp/uwsgi.ini:ro 120 | api: 121 | ... # other configuration 122 | volumes: 123 | ... # other volumes 124 | - ${PWD}/customization/configuration/custom_uwsgi.ini:/opt/openwisp/uwsgi.ini:ro 125 | 126 | .. _docker_nginx: 127 | 128 | Supplying Custom Nginx Configurations 129 | ------------------------------------- 130 | 131 | Docker 132 | ~~~~~~ 133 | 134 | 1. Create nginx your configuration file. 135 | 2. Set ``NGINX_CUSTOM_FILE`` to ``True`` in ``.env`` file. 136 | 3. Mount your file in ``docker-compose.yml`` as following: 137 | 138 | .. code-block:: yaml 139 | 140 | nginx: 141 | ... 142 | volumes: 143 | ... 144 | PATH/TO/YOUR/FILE:/etc/nginx/nginx.conf 145 | ... 146 | 147 | .. _docker_freeradius: 148 | 149 | Supplying Custom Freeradius Configurations 150 | ------------------------------------------ 151 | 152 | Note: ``/etc/raddb/clients.conf``, ``/etc/raddb/radiusd.conf``, 153 | ``/etc/raddb/sites-enabled/default``, ``/etc/raddb/mods-enabled/``, 154 | ``/etc/raddb/mods-available/`` are the default files you may want to 155 | overwrite and you can find all of default files in 156 | ``build/openwisp_freeradius/raddb``. The following are examples for 157 | including custom ``radiusd.conf`` and ``sites-enabled/default`` files. 158 | 159 | .. _docker-1: 160 | 161 | Docker 162 | ~~~~~~ 163 | 164 | 1. Create file configuration files that you want to edit / add to your 165 | container. 166 | 2. Mount your file in ``docker-compose.yml`` as following: 167 | 168 | .. code-block:: yaml 169 | 170 | nginx: 171 | ... 172 | volumes: 173 | ... 174 | PATH/TO/YOUR/RADIUSD:/etc/raddb/radiusd.conf 175 | PATH/TO/YOUR/DEFAULT:/etc/raddb/sites-enabled/default 176 | ... 177 | 178 | Supplying Custom Python Source Code 179 | ----------------------------------- 180 | 181 | You can build the images and supply custom python source code by creating 182 | a file named ``.build.env`` in the root of the repository, then set the 183 | variables inside ``.build.env`` file in ``=`` format. 184 | Multiple variable should be separated in newline. 185 | 186 | These are the variables that can be changed: 187 | 188 | - ``OPENWISP_MONITORING_SOURCE`` 189 | - ``OPENWISP_FIRMWARE_SOURCE`` 190 | - ``OPENWISP_CONTROLLER_SOURCE`` 191 | - ``OPENWISP_NOTIFICATION_SOURCE`` 192 | - ``OPENWISP_TOPOLOGY_SOURCE`` 193 | - ``OPENWISP_RADIUS_SOURCE`` 194 | - ``OPENWISP_IPAM_SOURCE`` 195 | - ``OPENWISP_USERS_SOURCE`` 196 | - ``OPENWISP_UTILS_SOURCE`` 197 | - ``DJANGO_X509_SOURCE`` 198 | - ``DJANGO_SOURCE`` 199 | 200 | For example, if you want to supply your own Django and :doc:`OpenWISP 201 | Controller ` source, your ``.build.env`` should be 202 | written like this: 203 | 204 | .. code-block:: shell 205 | 206 | DJANGO_SOURCE=https://github.com//Django/tarball/master 207 | OPENWISP_CONTROLLER_SOURCE=https://github.com//openwisp-controller/tarball/master 208 | 209 | Disabling Services 210 | ------------------ 211 | 212 | - ``openwisp-dashboard``: You cannot disable the openwisp-dashboard. It is 213 | the heart of OpenWISP and performs core functionalities. 214 | - ``openwisp-api``: You cannot disable the openwisp-api. It is required 215 | for interacting with your devices. 216 | - ``openwisp-websocket``: Removing this container will cause the system to 217 | not able to update real-time location for mobile devices. 218 | 219 | If you want to disable a service, you can simply remove the container for 220 | that service, however, there are additional steps for some images: 221 | 222 | - ``openwisp-network-topology``: Set the ``USE_OPENWISP_TOPOLOGY`` 223 | variable to ``False``. 224 | - ``openwisp-firmware-upgrader`` : Set the ``USE_OPENWISP_FIRMWARE`` 225 | variable to ``False``. 226 | - ``openwisp-monitoring`` : Set the ``USE_OPENWISP_MONITORING`` variable 227 | to ``False``. 228 | - ``openwisp-radius`` : Set the ``USE_OPENWISP_RADIUS`` variable to 229 | ``False``. 230 | - ``openwisp-postgres``: If you are using a separate database instance, 231 | 232 | - Ensure your database instance is reachable by the following OpenWISP 233 | containers: ``openvpn``, ``freeradius``, ``celerybeat``, ``celery``, 234 | ``celery_monitoring``, ``websocket``, ``api``, ``dashboard``. 235 | - Ensure your database server supports GeoDjango. (Install PostGIS for 236 | PostgreSQL) 237 | - Change the :ref:`PostgreSQL Database Setting 238 | ` to point to your instances, if you 239 | are using SSL, remember to set ``DB_SSLMODE``, ``DB_SSLKEY``, 240 | ``DB_SSLCERT``, ``DB_SSLROOTCERT``. 241 | - If you are using SSL, remember to mount volume containing the 242 | certificates and key in all the containers which contact the database 243 | server and make sure that the private key permission is ``600`` and 244 | owned by ``root:root``. 245 | - In your database, create database with name ````. 246 | 247 | - ``openwisp-postfix``: 248 | 249 | - Ensure your SMTP instance reachable by the OpenWISP containers. 250 | - Change the :ref:`email configuration variables ` to point 251 | to your instances. 252 | -------------------------------------------------------------------------------- /docs/user/faq.rst: -------------------------------------------------------------------------------- 1 | Docker OpenWISP FAQs 2 | ==================== 3 | 4 | .. contents:: **Table of Contents**: 5 | :depth: 1 6 | :local: 7 | 8 | 1. Setup fails, it couldn't find the images on DockerHub? 9 | --------------------------------------------------------- 10 | 11 | Answer: The setup requires following ports and destinations to be 12 | unblocked, if you are using a firewall or any external control to block 13 | traffic, please whitelist: 14 | 15 | = ====== ======== ========== ====================== ===================================== 16 | \ UserId Protocol DstPort Destination Process 17 | = ====== ======== ========== ====================== ===================================== 18 | 1 0 tcp,udp 443,53 gitlab.com ``/usr/bin/dockerd`` 19 | 2 0 tcp,udp 443,53 registry.gitlab.com ``/usr/bin/dockerd`` 20 | 3 0 tcp,udp 443,53 storage.googleapis.com ``/usr/bin/dockerd`` 21 | 4 0 udp 53 registry.gitlab.com ``/usr/bin/docker`` 22 | 5 0 tcp,udp 443,53 github.com ``/usr/lib/git-core/git-remote-http`` 23 | 6 0 tcp 443,80 172.18.0.0/16 ``/usr/bin/docker-proxy`` 24 | 7 0 udp 1812, 1813 172.18.0.0/16 ``/usr/bin/docker-proxy`` 25 | 8 0 tcp 25 172.18.0.0/16 ``/usr/bin/docker-proxy`` 26 | = ====== ======== ========== ====================== ===================================== 27 | 28 | 2. Makefile failed without any information, what's wrong? 29 | --------------------------------------------------------- 30 | 31 | Answer: You are using an old version of a requirement, please consider 32 | upgrading: 33 | 34 | .. code-block:: bash 35 | 36 | $ git --version 37 | git version 2.25.1 38 | $ docker --version 39 | Docker version 27.0.2, build 912c1dd 40 | $ docker compose version 41 | Docker Compose version v2.28.1 42 | $ make --version 43 | GNU Make 4.2.1 44 | $ bash --version 45 | GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu) 46 | $ uname -v # kernel-version 47 | #1 SMP Debian 4.19.181-1 (2021-03-19) 48 | 49 | 3. Can I run the containers as the ``root`` or ``docker`` 50 | --------------------------------------------------------- 51 | 52 | No, please do not run the Docker containers as these users. 53 | 54 | Ensure you use a less privileged user and tools like ``sudo`` or ``su`` to 55 | escalate privileges during the installation phase. 56 | -------------------------------------------------------------------------------- /docs/user/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start Guide 2 | ================= 3 | 4 | This page explains how to deploy OpenWISP using the docker images provided 5 | by Docker OpenWISP. 6 | 7 | .. contents:: **Table of Contents**: 8 | :depth: 1 9 | :local: 10 | 11 | Available Images 12 | ---------------- 13 | 14 | The images are hosted on `Docker Hub 15 | `__ and `GitLab Container Registry 16 | `__. 17 | 18 | Image Tags 19 | ~~~~~~~~~~ 20 | 21 | All images are tagged using the following convention: 22 | 23 | ====== ========================================================= 24 | Tag Software Version 25 | ====== ========================================================= 26 | latest This is the most recent official release of OpenWISP. 27 | 28 | On Github, this corresponds to the latest tagged release. 29 | edge This is the development version of OpenWISP. 30 | 31 | On Github, this corresponds to the current master branch. 32 | ====== ========================================================= 33 | 34 | Auto Install Script 35 | ------------------- 36 | 37 | .. figure:: ../images/auto-install.png 38 | :target: ../../_images/auto-install.png 39 | :alt: Auto Install Script for Docker OpenWISP 40 | 41 | The `auto-install 42 | `_ 43 | script can be used to quickly install an OpenWISP instance on your server. 44 | 45 | It will install the required system dependencies and start the docker 46 | containers. 47 | 48 | This script prompts the user for basic configuration parameters required 49 | to set up OpenWISP. Below are the prompts and their descriptions: 50 | 51 | - **OpenWISP Version:** Version of OpenWISP you want to install. If you 52 | leave this blank, the latest released version will be installed. 53 | - **.env File Path:** Path to an existing :doc:`".env" file ` 54 | file if you have one. If you leave this blank, the script will continue 55 | prompting for additional configuration. 56 | - **Domains:** The fully qualified domain names for the :ref:`Dashboard 57 | `, :ref:`API `, and :ref:`OpenVPN 58 | ` services. 59 | - **Site Manager Email:** Email address of the site manager. This email 60 | address will serve as the default sender address for all email 61 | communications from OpenWISP. 62 | - **Let's Encrypt Email:** Email address for Let's Encrypt to use for 63 | certificate generation. If you leave this blank, a self-signed 64 | certificate will be generated. 65 | 66 | .. include:: ../partials/updating-host-file.rst 67 | 68 | Run the following commands to download the `auto-install 69 | `_ 70 | script and execute it: 71 | 72 | .. code-block:: bash 73 | 74 | curl https://raw.githubusercontent.com/openwisp/docker-openwisp/master/deploy/auto-install.sh -o auto-install.sh 75 | sudo bash auto-install.sh 76 | 77 | The auto-install script maintains a log, which is useful for debugging or 78 | checking the real-time output of the script. You can view the log by 79 | running the following command: 80 | 81 | .. code-block:: bash 82 | 83 | tail -n 50 -f /opt/openwisp/autoinstall.log 84 | 85 | The auto-install script can be used to upgrade installations that were 86 | originally deployed using this script. You can upgrade your installation 87 | by using the following command 88 | 89 | .. code-block:: bash 90 | 91 | sudo bash auto-install.sh --upgrade 92 | 93 | .. note:: 94 | 95 | - If you're having any installation issues with the ``latest`` 96 | version, you can try auto-installation with the ``edge`` version, 97 | which ships the development version of OpenWISP. 98 | - Still facing errors while installation? Please :doc:`read the FAQ 99 | `. 100 | 101 | Using Docker Compose 102 | -------------------- 103 | 104 | This setup is suitable for single-server setup requirements. It is quicker 105 | and requires less prior knowledge about OpenWISP & networking. 106 | 107 | 1. Install requirements: 108 | 109 | .. code-block:: bash 110 | 111 | sudo apt -y update 112 | sudo apt -y install git docker.io make 113 | # Please ensure docker is installed properly and the following 114 | # command show system information. In most machines, you'll need to 115 | # add your user to the `docker` group and re-login to the shell. 116 | docker info 117 | 118 | 2. Setup repository: 119 | 120 | .. code-block:: bash 121 | 122 | git clone https://github.com/openwisp/docker-openwisp.git 123 | cd docker-openwisp 124 | 125 | 3. Configure: 126 | 127 | Please refer to the :doc:`settings` and :doc:`customization` pages to 128 | configure any aspect of your OpenWISP instance. 129 | 130 | Make sure to change the values for :ref:`essential 131 | ` and :ref:`security ` 132 | variables. 133 | 134 | 4. Deploy: 135 | 136 | Use the ``make start`` command to pull images and start the containers. 137 | 138 | .. note:: 139 | 140 | If you want to shutdown services for maintenance or any other 141 | purposes, please use ``make stop``. 142 | 143 | If you are facing errors during the installation process, :doc:`read the 144 | FAQ ` for known issues. 145 | -------------------------------------------------------------------------------- /images/common/init_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # OpenWISP common module init script 3 | set -e 4 | source utils.sh 5 | 6 | init_conf 7 | 8 | # Start services 9 | if [ "$MODULE_NAME" = 'dashboard' ]; then 10 | if [ "$OPENWISP_GEOCODING_CHECK" = 'True' ]; then 11 | python manage.py check --deploy --tag geocoding 12 | fi 13 | python services.py database redis 14 | python manage.py migrate --noinput 15 | test -f "$SSH_PRIVATE_KEY_PATH" || ssh-keygen -t ed25519 -f "$SSH_PRIVATE_KEY_PATH" -N "" 16 | python load_init_data.py 17 | python manage.py collectstatic --noinput 18 | start_uwsgi 19 | elif [ "$MODULE_NAME" = 'postfix' ]; then 20 | postfix_config 21 | postfix start 22 | rsyslogd -n 23 | elif [ "$MODULE_NAME" = 'freeradius' ]; then 24 | wait_nginx_services 25 | if [ "$DEBUG_MODE" = 'False' ]; then 26 | source docker-entrypoint.sh 27 | else 28 | source docker-entrypoint.sh -X 29 | fi 30 | elif [ "$MODULE_NAME" = 'openvpn' ]; then 31 | if [[ -z "$VPN_DOMAIN" ]]; then exit; fi 32 | wait_nginx_services 33 | openvpn_preconfig 34 | openvpn_config 35 | openvpn_config_download 36 | crl_download 37 | echo "*/1 * * * * sh /openvpn.sh" | crontab - 38 | ( 39 | crontab -l 40 | echo "0 0 * * * sh /revokelist.sh" 41 | ) | crontab - 42 | crond 43 | # Schedule send topology script only when 44 | # network topology module is enabled. 45 | if [ "$USE_OPENWISP_TOPOLOGY" == "True" ]; then 46 | init_send_network_topology 47 | fi 48 | # Supervisor is used to start the service because OpenVPN 49 | # needs to restart after crl list is updated or configurations 50 | # are changed. If OpenVPN as the service keeping the 51 | # docker container running, restarting would mean killing 52 | # the container while supervisor helps only to restart the service! 53 | supervisord --nodaemon --configuration supervisord.conf 54 | elif [ "$MODULE_NAME" = 'nginx' ]; then 55 | rm -rf /etc/nginx/conf.d/default.conf 56 | if [ "$NGINX_CUSTOM_FILE" = 'True' ]; then 57 | nginx -g 'daemon off;' 58 | fi 59 | envsubst /etc/nginx/nginx.conf 60 | envsubst_create_config /etc/nginx/openwisp.internal.template.conf internal INTERNAL 61 | if [ "$SSL_CERT_MODE" = 'Yes' ]; then 62 | nginx_prod 63 | elif [ "$SSL_CERT_MODE" = 'SelfSigned' ]; then 64 | nginx_dev 65 | else 66 | envsubst_create_config /etc/nginx/openwisp.template.conf http DOMAIN 67 | fi 68 | nginx -g 'daemon off;' 69 | elif [ "$MODULE_NAME" = 'celery' ]; then 70 | python services.py database redis dashboard 71 | echo "Starting the 'default' celery worker" 72 | celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues celery \ 73 | -n celery@%h --logfile /opt/openwisp/logs/celery.log \ 74 | --pidfile /opt/openwisp/celery.pid --detach \ 75 | ${OPENWISP_CELERY_COMMAND_FLAGS} 76 | 77 | if [ "$USE_OPENWISP_CELERY_NETWORK" = "True" ]; then 78 | echo "Starting the 'network' celery worker" 79 | celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues network \ 80 | -n network@%h --logfile /opt/openwisp/logs/celery_network.log \ 81 | --pidfile /opt/openwisp/celery_network.pid --detach \ 82 | ${OPENWISP_CELERY_NETWORK_COMMAND_FLAGS} 83 | fi 84 | 85 | if [[ "$USE_OPENWISP_FIRMWARE" == "True" && "$USE_OPENWISP_CELERY_FIRMWARE" == "True" ]]; then 86 | echo "Starting the 'firmware_upgrader' celery worker" 87 | celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues firmware_upgrader \ 88 | -n firmware_upgrader@%h --logfile /opt/openwisp/logs/celery_firmware_upgrader.log \ 89 | --pidfile /opt/openwisp/celery_firmware_upgrader.pid --detach \ 90 | ${OPENWISP_CELERY_FIRMWARE_COMMAND_FLAGS} 91 | fi 92 | sleep 1s 93 | tail -f /opt/openwisp/logs/* 94 | elif [ "$MODULE_NAME" = 'celery_monitoring' ]; then 95 | python services.py database redis dashboard 96 | if [[ "$USE_OPENWISP_MONITORING" == "True" && "$USE_OPENWISP_CELERY_MONITORING" == 'True' ]]; then 97 | echo "Starting the 'monitoring' celery worker" 98 | celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues monitoring \ 99 | -n monitoring@%h --logfile /opt/openwisp/logs/celery_monitoring.log \ 100 | --pidfile /opt/openwisp/celery_monitoring.pid --detach \ 101 | ${OPENWISP_CELERY_MONITORING_COMMAND_FLAGS} 102 | echo "Starting the 'monitoring_checks' celery worker" 103 | celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues monitoring_checks \ 104 | -n monitoring_checks@%h --logfile /opt/openwisp/logs/celery_monitoring_checks.log \ 105 | --pidfile /opt/openwisp/celery_monitoring_checks.pid --detach \ 106 | ${OPENWISP_CELERY_MONITORING_CHECKS_COMMAND_FLAGS} 107 | sleep 1s 108 | tail -f /opt/openwisp/logs/* 109 | else 110 | echo "Monitoring queues are not activated, exiting." 111 | fi 112 | elif [ "$MODULE_NAME" = 'celerybeat' ]; then 113 | rm -rf celerybeat.pid 114 | python services.py database redis dashboard 115 | celery -A openwisp beat -l ${DJANGO_LOG_LEVEL} 116 | else 117 | python services.py database redis dashboard 118 | start_uwsgi 119 | fi 120 | -------------------------------------------------------------------------------- /images/common/manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp.settings") 6 | 7 | from django.core.management import execute_from_command_line 8 | 9 | execute_from_command_line(sys.argv) 10 | -------------------------------------------------------------------------------- /images/common/openwisp/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from .celery import app as celery_app 4 | 5 | __all__ = ["celery_app"] 6 | __openwisp_version__ = "25.07.0a" 7 | __openwisp_installation_method__ = "docker-openwisp" 8 | -------------------------------------------------------------------------------- /images/common/openwisp/asgi.py: -------------------------------------------------------------------------------- 1 | """ASGI entrypoint. 2 | 3 | Configures Django and then runs the application defined in the 4 | ASGI_APPLICATION setting. 5 | """ 6 | 7 | import os 8 | 9 | from channels.auth import AuthMiddlewareStack 10 | from channels.routing import ProtocolTypeRouter, URLRouter 11 | from channels.security.websocket import AllowedHostsOriginValidator 12 | from django.core.asgi import get_asgi_application 13 | from openwisp.utils import env_bool 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp.settings") 16 | django_asgi_app = get_asgi_application() 17 | 18 | from openwisp_controller.routing import ( # noqa: E402 19 | get_routes as get_controller_routes, 20 | ) 21 | 22 | routes = get_controller_routes() 23 | 24 | if env_bool(os.environ.get("USE_OPENWISP_TOPOLOGY")): 25 | from openwisp_network_topology.routing import ( # noqa: E402 26 | websocket_urlpatterns as network_topology_routes, 27 | ) 28 | 29 | routes.extend(network_topology_routes) 30 | 31 | application = ProtocolTypeRouter( 32 | { 33 | "http": django_asgi_app, 34 | "websocket": AllowedHostsOriginValidator( 35 | AuthMiddlewareStack(URLRouter(routes)) 36 | ), 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /images/common/openwisp/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | from celery.schedules import crontab 5 | from django.utils.timezone import timedelta 6 | from openwisp.utils import env_bool 7 | 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp.settings") 9 | 10 | radius_schedule, topology_schedule, monitoring_schedule, metric_collection_schedule = ( 11 | {}, 12 | {}, 13 | {}, 14 | {}, 15 | ) 16 | task_routes = {} 17 | 18 | if env_bool(os.environ.get("USE_OPENWISP_CELERY_NETWORK")): 19 | task_routes["openwisp_controller.connection.tasks.*"] = {"queue": "network"} 20 | 21 | if env_bool(os.environ.get("USE_OPENWISP_MONITORING")): 22 | monitoring_schedule = { 23 | "run_checks": { 24 | "task": "openwisp_monitoring.check.tasks.run_checks", 25 | "schedule": timedelta(minutes=5), 26 | }, 27 | } 28 | if env_bool(os.environ.get("USE_OPENWISP_CELERY_MONITORING")): 29 | task_routes["openwisp_monitoring.check.tasks.perform_check"] = { 30 | "queue": "monitoring_checks" 31 | } 32 | task_routes["openwisp_monitoring.monitoring.tasks.*"] = {"queue": "monitoring"} 33 | 34 | if env_bool(os.environ.get("USE_OPENWISP_FIRMWARE")) and env_bool( 35 | os.environ.get("USE_OPENWISP_CELERY_FIRMWARE") 36 | ): 37 | task_routes["openwisp_firmware_upgrader.tasks.upgrade_firmware"] = { 38 | "queue": "firmware_upgrader" 39 | } 40 | task_routes["openwisp_firmware_upgrader.tasks.batch_upgrade_operation"] = { 41 | "queue": "firmware_upgrader" 42 | } 43 | 44 | if env_bool(os.environ.get("USE_OPENWISP_RADIUS")): 45 | radius_schedule = { 46 | "radius-periodic-tasks": { 47 | "task": "openwisp.tasks.radius_tasks", 48 | "schedule": crontab(minute=30, hour=3), 49 | "args": (), 50 | }, 51 | } 52 | 53 | if env_bool(os.environ.get("USE_OPENWISP_TOPOLOGY")): 54 | topology_schedule = { 55 | "topology-snapshot-tasks": { 56 | "task": "openwisp.tasks.save_snapshot", 57 | "schedule": crontab(minute=45, hour=23), 58 | "args": (), 59 | }, 60 | "topology-periodic-tasks": { 61 | "task": "openwisp.tasks.update_topology", 62 | "schedule": crontab(minute="*/5"), 63 | "args": (), 64 | }, 65 | } 66 | 67 | if env_bool(os.environ.get("METRIC_COLLECTION", "True")): 68 | metric_collection_schedule = { 69 | "send_usage_metrics": { 70 | "task": "openwisp_utils.metric_collection.tasks.send_usage_metrics", 71 | "schedule": timedelta(days=1), 72 | }, 73 | } 74 | 75 | notification_schedule = { 76 | "notification-delete-tasks": { 77 | "task": "openwisp_notifications.tasks.delete_old_notifications", 78 | "schedule": crontab(minute=00, hour=23), 79 | "args": (90,), 80 | }, 81 | } 82 | 83 | if not os.environ.get("USE_OPENWISP_CELERY_TASK_ROUTES_DEFAULTS", True): 84 | task_routes = {} 85 | 86 | app = Celery( 87 | "openwisp", 88 | include=["openwisp.tasks"], 89 | task_routes=task_routes, 90 | beat_schedule={ 91 | **radius_schedule, 92 | **topology_schedule, 93 | **notification_schedule, 94 | **monitoring_schedule, 95 | **metric_collection_schedule, 96 | }, 97 | ) 98 | app.config_from_object("django.conf:settings", namespace="CELERY") 99 | app.autodiscover_tasks() 100 | -------------------------------------------------------------------------------- /images/common/openwisp/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | from channels.security.websocket import AllowedHostsOriginValidator 4 | from django.core.asgi import get_asgi_application 5 | from openwisp_controller.routing import get_routes 6 | 7 | application = ProtocolTypeRouter( 8 | { 9 | "websocket": AllowedHostsOriginValidator( 10 | AuthMiddlewareStack(URLRouter(get_routes())) 11 | ), 12 | "http": get_asgi_application(), 13 | } 14 | ) 15 | -------------------------------------------------------------------------------- /images/common/openwisp/settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | from urllib.parse import quote 6 | 7 | import tldextract 8 | from openwisp.utils import ( 9 | env_bool, 10 | is_string_env_bool, 11 | is_string_env_json, 12 | request_scheme, 13 | ) 14 | 15 | # Read all the env variables and set them as django configuration. 16 | for config in os.environ: 17 | if "OPENWISP_" in config: 18 | value = os.environ[config] 19 | if value.isdigit(): 20 | globals()[config] = int(value) 21 | elif is_string_env_bool(value): 22 | globals()[config] = env_bool(value) 23 | elif value == "None": 24 | globals()[config] = None 25 | elif is_string_env_json(value): 26 | globals()[config] = json.loads(value) 27 | else: 28 | globals()[config] = value 29 | 30 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 31 | SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] 32 | DEBUG = env_bool(os.environ["DEBUG_MODE"]) 33 | MAX_REQUEST_SIZE = int(os.environ["NGINX_CLIENT_BODY_SIZE"]) * 1024 * 1024 34 | ROOT_DOMAIN = "." + tldextract.extract(os.environ["DASHBOARD_DOMAIN"]).registered_domain 35 | INSTALLED_APPS = [] 36 | 37 | if "DJANGO_ALLOWED_HOSTS" not in os.environ: 38 | os.environ["DJANGO_ALLOWED_HOSTS"] = ROOT_DOMAIN 39 | 40 | ALLOWED_HOSTS = [ 41 | "localhost", 42 | os.environ["DASHBOARD_APP_SERVICE"], 43 | os.environ["DASHBOARD_INTERNAL"], 44 | os.environ["API_INTERNAL"], 45 | ] + os.environ["DJANGO_ALLOWED_HOSTS"].split(",") 46 | 47 | AUTH_USER_MODEL = "openwisp_users.User" 48 | SITE_ID = 1 49 | LOGIN_REDIRECT_URL = "admin:index" 50 | ACCOUNT_LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL 51 | ROOT_URLCONF = "openwisp.urls" 52 | HTTP_SCHEME = request_scheme() 53 | 54 | # CORS 55 | CORS_ALLOWED_ORIGINS = [ 56 | f'{HTTP_SCHEME}://{os.environ["DASHBOARD_DOMAIN"]}', 57 | f'{HTTP_SCHEME}://{os.environ["API_DOMAIN"]}', 58 | ] + os.environ["DJANGO_CORS_HOSTS"].split(",") 59 | CORS_ALLOW_CREDENTIALS = True 60 | 61 | if HTTP_SCHEME == "https": 62 | SESSION_COOKIE_SECURE = True 63 | CSRF_COOKIE_SECURE = True 64 | if HTTP_SCHEME == "http": 65 | DJANGO_LOCI_GEOCODE_STRICT_TEST = False 66 | 67 | STATICFILES_FINDERS = [ 68 | "django.contrib.staticfiles.finders.FileSystemFinder", 69 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 70 | "openwisp_utils.staticfiles.DependencyFinder", 71 | ] 72 | 73 | MIDDLEWARE = [ 74 | "corsheaders.middleware.CorsMiddleware", 75 | "django.middleware.security.SecurityMiddleware", 76 | "django.contrib.sessions.middleware.SessionMiddleware", 77 | "django.middleware.common.CommonMiddleware", 78 | "django.middleware.csrf.CsrfViewMiddleware", 79 | "django.contrib.auth.middleware.AuthenticationMiddleware", 80 | "allauth.account.middleware.AccountMiddleware", 81 | "django.contrib.messages.middleware.MessageMiddleware", 82 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 83 | ] 84 | 85 | AUTHENTICATION_BACKENDS = [ 86 | "openwisp_users.backends.UsersAuthenticationBackend", 87 | ] 88 | 89 | TEMPLATES = [ 90 | { 91 | "BACKEND": "django.template.backends.django.DjangoTemplates", 92 | "DIRS": [os.path.join(BASE_DIR, "templates")], 93 | "OPTIONS": { 94 | "loaders": [ 95 | ( 96 | "django.template.loaders.cached.Loader", 97 | [ 98 | "django.template.loaders.filesystem.Loader", 99 | "django.template.loaders.app_directories.Loader", 100 | "openwisp_utils.loaders.DependencyLoader", 101 | ], 102 | ), 103 | ], 104 | "context_processors": [ 105 | "django.template.context_processors.request", 106 | "django.contrib.auth.context_processors.auth", 107 | "django.contrib.messages.context_processors.messages", 108 | ], 109 | }, 110 | }, 111 | ] 112 | 113 | if DEBUG: 114 | TEMPLATES[0]["OPTIONS"]["context_processors"].insert( 115 | 0, "django.template.context_processors.debug" 116 | ) 117 | 118 | if os.environ["MODULE_NAME"] == "dashboard": 119 | TEMPLATES[0]["OPTIONS"]["context_processors"].extend( 120 | [ 121 | "openwisp_utils.admin_theme.context_processor.menu_groups", 122 | "openwisp_utils.admin_theme.context_processor.admin_theme_settings", 123 | "openwisp_notifications.context_processors.notification_api_settings", 124 | ] 125 | ) 126 | 127 | FORM_RENDERER = "django.forms.renderers.TemplatesSetting" 128 | 129 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 130 | SESSION_CACHE_ALIAS = "default" 131 | SESSION_COOKIE_DOMAIN = ROOT_DOMAIN 132 | 133 | # Required for API request from Django admin 134 | CSRF_COOKIE_DOMAIN = ROOT_DOMAIN 135 | CSRF_TRUSTED_ORIGINS = CORS_ALLOWED_ORIGINS 136 | 137 | WSGI_APPLICATION = "openwisp.wsgi.application" 138 | ASGI_APPLICATION = "openwisp.asgi.application" 139 | 140 | REDIS_HOST = os.environ["REDIS_HOST"] 141 | REDIS_PORT = os.environ.get("REDIS_PORT", 6379) 142 | REDIS_USER = os.environ.get("REDIS_USER") 143 | REDIS_PASS = os.environ.get("REDIS_PASS") 144 | REDIS_SCHEME = ( 145 | "rediss" if env_bool(os.environ.get("REDIS_USE_TLS", "False")) else "redis" 146 | ) 147 | 148 | # Build base Redis URL 149 | 150 | if REDIS_USER and REDIS_PASS: 151 | credentials = f"{quote(REDIS_USER)}:{quote(REDIS_PASS)}@" 152 | elif REDIS_PASS: 153 | # Password only 154 | credentials = f":{quote(REDIS_PASS)}@" 155 | else: 156 | credentials = "" 157 | REDIS_BASE_URL = f"{REDIS_SCHEME}://{credentials}{REDIS_HOST}:{REDIS_PORT}" 158 | 159 | REDIS_CACHE_URL = os.environ.get("REDIS_CACHE_URL", f"{REDIS_BASE_URL}/0") 160 | CHANNEL_REDIS_HOST = os.environ.get("CHANNEL_REDIS_URL", f"{REDIS_BASE_URL}/1") 161 | CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", f"{REDIS_BASE_URL}/2") 162 | 163 | CELERY_TASK_ACKS_LATE = True 164 | CELERY_WORKER_PREFETCH_MULTIPLIER = 1 165 | CELERY_BROKER_TRANSPORT_OPTIONS = {"max_retries": 10} 166 | if env_bool(os.environ.get("REDIS_USE_TLS", "False")): 167 | import ssl 168 | 169 | CELERY_BROKER_USE_SSL = { 170 | "ssl_cert_reqs": ssl.CERT_REQUIRED, 171 | } 172 | 173 | # Database 174 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 175 | 176 | DB_OPTIONS = { 177 | "sslmode": os.environ["DB_SSLMODE"], 178 | "sslkey": os.environ["DB_SSLKEY"], 179 | "sslcert": os.environ["DB_SSLCERT"], 180 | "sslrootcert": os.environ["DB_SSLROOTCERT"], 181 | } 182 | DB_OPTIONS.update(json.loads(os.environ["DB_OPTIONS"])) 183 | 184 | DATABASES = { 185 | "default": { 186 | "ENGINE": os.environ["DB_ENGINE"], 187 | "NAME": os.environ["DB_NAME"], 188 | "USER": os.environ["DB_USER"], 189 | "PASSWORD": os.environ["DB_PASS"], 190 | "HOST": os.environ["DB_HOST"], 191 | "PORT": os.environ["DB_PORT"], 192 | "OPTIONS": DB_OPTIONS, 193 | }, 194 | } 195 | 196 | TIMESERIES_DATABASE = { 197 | "BACKEND": "openwisp_monitoring.db.backends.influxdb", 198 | "USER": os.environ["INFLUXDB_USER"], 199 | "PASSWORD": os.environ["INFLUXDB_PASS"], 200 | "NAME": os.environ["INFLUXDB_NAME"], 201 | "HOST": os.environ["INFLUXDB_HOST"], 202 | "PORT": os.environ["INFLUXDB_PORT"], 203 | } 204 | OPENWISP_MONITORING_DEFAULT_RETENTION_POLICY = os.environ[ 205 | "INFLUXDB_DEFAULT_RETENTION_POLICY" 206 | ] 207 | 208 | # Channels(Websocket) 209 | # https://channels.readthedocs.io/en/latest/topics/channel_layers.html#configuration 210 | CHANNEL_LAYERS = { 211 | "default": { 212 | "BACKEND": "channels_redis.core.RedisChannelLayer", 213 | "CONFIG": {"hosts": [CHANNEL_REDIS_HOST]}, 214 | }, 215 | } 216 | 217 | # Cache 218 | # https://docs.djangoproject.com/en/2.2/ref/settings/#caches 219 | 220 | CACHES = { 221 | "default": { 222 | "BACKEND": "django_redis.cache.RedisCache", 223 | "LOCATION": REDIS_CACHE_URL, 224 | "OPTIONS": { 225 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 226 | }, 227 | } 228 | } 229 | 230 | if REDIS_PASS: 231 | CACHES["default"]["OPTIONS"]["PASSWORD"] = os.environ["REDIS_PASS"] 232 | 233 | # Leaflet Configurations 234 | # https://django-leaflet.readthedocs.io/en/latest/templates.html#configuration 235 | 236 | LEAFLET_CONFIG = { 237 | "DEFAULT_CENTER": [ 238 | int(os.environ["DJANGO_LEAFET_CENTER_X_AXIS"]), 239 | int(os.environ["DJANGO_LEAFET_CENTER_Y_AXIS"]), 240 | ], 241 | "RESET_VIEW": False, 242 | "DEFAULT_ZOOM": int(os.environ["DJANGO_LEAFET_ZOOM"]), 243 | } 244 | 245 | # Password validation 246 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 247 | 248 | AUTH_PASSWORD_VALIDATORS = [ 249 | { 250 | "NAME": ( 251 | "django.contrib.auth.password_validation." 252 | "UserAttributeSimilarityValidator" 253 | ) 254 | }, 255 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 256 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 257 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 258 | ] 259 | 260 | # Internationalization 261 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 262 | 263 | LANGUAGE_CODE = os.environ["DJANGO_LANGUAGE_CODE"] 264 | TIME_ZONE = os.environ["TZ"] 265 | USE_I18N = True 266 | USE_TZ = True 267 | 268 | # Static files (CSS, JavaScript, Images) 269 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 270 | 271 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 272 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 273 | # PRIVATE_STORAGE_ROOT path should be similar to ansible-openwisp2 274 | PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, "private") 275 | STATIC_URL = "/static/" 276 | MEDIA_URL = "/media/" 277 | 278 | # Email Configurations 279 | 280 | DEFAULT_FROM_EMAIL = os.environ["EMAIL_DJANGO_DEFAULT"] 281 | EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] 282 | EMAIL_HOST = os.environ["EMAIL_HOST"] 283 | EMAIL_PORT = os.environ["EMAIL_HOST_PORT"] 284 | EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"] 285 | EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"] 286 | EMAIL_USE_TLS = env_bool(os.environ["EMAIL_HOST_TLS"]) 287 | EMAIL_TIMEOUT = int(os.environ["EMAIL_TIMEOUT"]) 288 | 289 | # Logging 290 | # http://docs.djangoproject.com/en/dev/topics/logging 291 | 292 | LOGGING = { 293 | "version": 1, 294 | "disable_existing_loggers": False, 295 | "filters": { 296 | "user_filter": { 297 | "()": "openwisp.utils.HostFilter", 298 | }, 299 | "require_debug_false": { 300 | "()": "django.utils.log.RequireDebugFalse", 301 | }, 302 | }, 303 | "formatters": { 304 | "verbose": { 305 | "format": ( 306 | "\n[%(host)s] - %(levelname)s, time: [%(asctime)s]," 307 | "process: %(process)d, thread: %(thread)d\n%(message)s" 308 | ) 309 | }, 310 | }, 311 | "handlers": { 312 | "console": { 313 | "level": os.environ["DJANGO_LOG_LEVEL"], 314 | "class": "logging.StreamHandler", 315 | "filters": ["user_filter"], 316 | "formatter": "verbose", 317 | "stream": sys.stdout, 318 | }, 319 | "mail_admins": { 320 | "level": os.environ["DJANGO_LOG_LEVEL"], 321 | "class": "django.utils.log.AdminEmailHandler", 322 | "filters": ["require_debug_false", "user_filter"], 323 | }, 324 | "null": { 325 | "level": os.environ["DJANGO_LOG_LEVEL"], 326 | "class": "logging.NullHandler", 327 | "filters": ["user_filter"], 328 | "formatter": "verbose", 329 | }, 330 | }, 331 | "root": { 332 | "level": os.environ["DJANGO_LOG_LEVEL"], 333 | "handlers": [ 334 | "console", 335 | "mail_admins", 336 | ], 337 | }, 338 | "loggers": { 339 | "pre_django_setup": { 340 | "level": os.environ["DJANGO_LOG_LEVEL"], 341 | "handlers": ["console"], 342 | "propagate": False, 343 | } 344 | }, 345 | } 346 | 347 | # Sentry 348 | # https://sentry.io/for/django/ 349 | 350 | if os.environ["DJANGO_SENTRY_DSN"]: 351 | import sentry_sdk 352 | from sentry_sdk.integrations.django import DjangoIntegration 353 | 354 | sentry_sdk.init( 355 | dsn=os.environ["DJANGO_SENTRY_DSN"], integrations=[DjangoIntegration()] 356 | ) 357 | 358 | # OpenWISP Modules's configurations 359 | OPENWISP_FIRMWARE_UPGRADER_MAX_FILE_SIZE = MAX_REQUEST_SIZE 360 | DJANGO_X509_DEFAULT_CERT_VALIDITY = int(os.environ["DJANGO_X509_DEFAULT_CERT_VALIDITY"]) 361 | DJANGO_X509_DEFAULT_CA_VALIDITY = int(os.environ["DJANGO_X509_DEFAULT_CA_VALIDITY"]) 362 | SOCIALACCOUNT_PROVIDERS = { 363 | "facebook": { 364 | "METHOD": "oauth2", 365 | "SCOPE": ["email", "public_profile"], 366 | "AUTH_PARAMS": {"auth_type": "reauthenticate"}, 367 | "INIT_PARAMS": {"cookie": True}, 368 | "FIELDS": ["id", "email", "name", "first_name", "last_name", "verified"], 369 | "VERIFIED_EMAIL": True, 370 | }, 371 | "google": { 372 | "SCOPE": ["profile", "email"], 373 | "AUTH_PARAMS": {"access_type": "online"}, 374 | }, 375 | } 376 | 377 | TEST_RUNNER = "openwisp_utils.metric_collection.tests.runner.MockRequestPostRunner" 378 | 379 | # Add Custom OpenWrt Images for openwisp firmware 380 | try: 381 | OPENWRT_IMAGES = json.loads(os.environ["OPENWISP_CUSTOM_OPENWRT_IMAGES"]) 382 | except (json.decoder.JSONDecodeError, TypeError): 383 | OPENWISP_CUSTOM_OPENWRT_IMAGES = None 384 | # Key is defined but it's not a proper JSON, probably user 385 | # needs to read the docs, so let's inform them. 386 | logging.warning( 387 | 'Could not load "OPENWISP_CUSTOM_OPENWRT_IMAGES" please read ' 388 | "the docs to configure it properly, continuing without it." 389 | ) 390 | except KeyError: 391 | # Key is not defined, that's okay, default is None. 392 | pass 393 | else: 394 | OPENWISP_CUSTOM_OPENWRT_IMAGES = list() 395 | for image in OPENWRT_IMAGES: 396 | OPENWISP_CUSTOM_OPENWRT_IMAGES += ( 397 | ( 398 | image["name"], 399 | {"label": image["label"], "boards": tuple(image["boards"])}, 400 | ), 401 | ) 402 | 403 | try: 404 | from openwisp.module_settings import * # noqa: F401, F403 405 | except ImportError: 406 | pass 407 | 408 | if env_bool(os.environ["USE_OPENWISP_RADIUS"]): 409 | REST_AUTH = { 410 | "SESSION_LOGIN": False, 411 | "PASSWORD_RESET_SERIALIZER": ( 412 | "openwisp_radius.api.serializers.PasswordResetSerializer" 413 | ), 414 | "REGISTER_SERIALIZER": "openwisp_radius.api.serializers.RegisterSerializer", 415 | } 416 | OPENWISP_RADIUS_FREERADIUS_ALLOWED_HOSTS = os.environ[ 417 | "OPENWISP_RADIUS_FREERADIUS_ALLOWED_HOSTS" 418 | ].split(",") 419 | elif "openwisp_radius" in INSTALLED_APPS: 420 | INSTALLED_APPS.remove("openwisp_radius") 421 | 422 | if ( 423 | not env_bool(os.environ["USE_OPENWISP_TOPOLOGY"]) 424 | and "openwisp_network_topology" in INSTALLED_APPS 425 | ): 426 | INSTALLED_APPS.remove("openwisp_network_topology") 427 | if ( 428 | not env_bool(os.environ["USE_OPENWISP_FIRMWARE"]) 429 | and "openwisp_firmware_upgrader" in INSTALLED_APPS 430 | ): 431 | INSTALLED_APPS.remove("openwisp_firmware_upgrader") 432 | if not env_bool(os.environ["USE_OPENWISP_MONITORING"]): 433 | if "openwisp_monitoring.monitoring" in INSTALLED_APPS: 434 | INSTALLED_APPS.remove("openwisp_monitoring.monitoring") 435 | if "openwisp_monitoring.device" in INSTALLED_APPS: 436 | INSTALLED_APPS.remove("openwisp_monitoring.device") 437 | if "openwisp_monitoring.check" in INSTALLED_APPS: 438 | INSTALLED_APPS.remove("openwisp_monitoring.check") 439 | if EMAIL_BACKEND == "djcelery_email.backends.CeleryEmailBackend": 440 | INSTALLED_APPS.append("djcelery_email") 441 | if env_bool(os.environ.get("METRIC_COLLECTION", "True")): 442 | INSTALLED_APPS.append("openwisp_utils.metric_collection") 443 | 444 | try: 445 | from .configuration.custom_django_settings import * # noqa: F403, F401 446 | except ImportError: 447 | pass 448 | -------------------------------------------------------------------------------- /images/common/openwisp/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | 5 | from celery import shared_task 6 | from django.core import management 7 | 8 | 9 | @shared_task 10 | def radius_tasks(): 11 | management.call_command( 12 | "delete_old_radacct", int(os.environ["CRON_DELETE_OLD_RADACCT"]) 13 | ) 14 | management.call_command( 15 | "delete_old_postauth", int(os.environ["CRON_DELETE_OLD_POSTAUTH"]) 16 | ) 17 | management.call_command( 18 | "cleanup_stale_radacct", int(os.environ["CRON_CLEANUP_STALE_RADACCT"]) 19 | ) 20 | management.call_command("deactivate_expired_users") 21 | management.call_command( 22 | "delete_old_radiusbatch_users", 23 | older_than_days=int(os.environ["CRON_DELETE_OLD_RADIUSBATCH_USERS"]), 24 | ) 25 | 26 | 27 | @shared_task 28 | def save_snapshot(): 29 | management.call_command("save_snapshot") 30 | 31 | 32 | @shared_task 33 | def update_topology(): 34 | management.call_command("update_topology") 35 | -------------------------------------------------------------------------------- /images/common/openwisp/utils.py: -------------------------------------------------------------------------------- 1 | # Utility functions for django modules 2 | # that are used in multiple openwisp modules 3 | import json 4 | import logging 5 | import os 6 | import socket 7 | 8 | 9 | class HostFilter(logging.Filter): 10 | # Used in logging for printing hostname 11 | # of the container with log details 12 | def filter(self, record): 13 | record.host = socket.gethostname() 14 | return True 15 | 16 | 17 | def is_string_env_json(env_json): 18 | try: 19 | json.loads(env_json) 20 | except ValueError: 21 | return False 22 | return True 23 | 24 | 25 | def is_string_env_bool(env): 26 | return env.lower() in ["true", "yes", "false", "no"] 27 | 28 | 29 | def env_bool(env): 30 | return env.lower() in ["true", "yes"] 31 | 32 | 33 | def request_scheme(): 34 | # os.environ['SSL_CERT_MODE'] can have different 35 | # values: True | False | External | SelfSigned 36 | if os.environ["SSL_CERT_MODE"] in ["False", "false", "FALSE", "No", "no", "NO"]: 37 | return "http" 38 | return "https" 39 | 40 | 41 | def openwisp_controller_urls(): 42 | # Setting correct urlpatterns for the 43 | # modules -- used in urls.py 44 | from openwisp_controller.urls import urlpatterns as controller_urls 45 | 46 | exclude = ["openwisp_users.accounts.urls"] 47 | for url in controller_urls[:]: 48 | if url.urlconf_module.__name__ in exclude: 49 | controller_urls.remove(url) 50 | return controller_urls 51 | -------------------------------------------------------------------------------- /images/common/openwisp/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /images/common/services.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | import time 6 | 7 | from utils import uwsgi_curl 8 | 9 | 10 | def database_status(): 11 | try: 12 | psycopg2.connect( 13 | dbname=os.environ["DB_NAME"], 14 | user=os.environ["DB_USER"], 15 | password=os.environ["DB_PASS"], 16 | host=os.environ["DB_HOST"], 17 | port=os.environ["DB_PORT"], 18 | sslmode=os.environ["DB_SSLMODE"], 19 | sslcert=os.environ["DB_SSLCERT"], 20 | sslkey=os.environ["DB_SSLKEY"], 21 | sslrootcert=os.environ["DB_SSLROOTCERT"], 22 | ) 23 | except psycopg2.OperationalError: 24 | time.sleep(3) 25 | return False 26 | else: 27 | return True 28 | 29 | 30 | def uwsgi_status(target, exit_on_error=False): 31 | try: 32 | uwsgi_curl(target) 33 | except OSError: 34 | # used for readiness/liveliness probes 35 | if exit_on_error: 36 | sys.exit(1) 37 | time.sleep(3) 38 | return False 39 | else: 40 | return True 41 | 42 | 43 | def dashboard_status(): 44 | t = f"{os.environ['DASHBOARD_APP_SERVICE']}:{os.environ['DASHBOARD_APP_PORT']}" 45 | return uwsgi_status(t) 46 | 47 | 48 | def redis_status(): 49 | kwargs = {} 50 | redis_user = os.environ.get("REDIS_USER") 51 | redis_pass = os.environ.get("REDIS_PASS") 52 | redis_port = os.environ.get("REDIS_PORT", 6379) 53 | redis_use_tls = os.environ.get("REDIS_USE_TLS", "False").lower() == "true" 54 | 55 | if redis_user: 56 | kwargs["username"] = redis_user 57 | if redis_pass: 58 | kwargs["password"] = redis_pass 59 | if redis_port: 60 | kwargs["port"] = redis_port 61 | if redis_use_tls: 62 | kwargs["ssl"] = redis_use_tls 63 | rs = redis.Redis(os.environ["REDIS_HOST"], **kwargs) 64 | try: 65 | rs.ping() 66 | except redis.ConnectionError: 67 | time.sleep(3) 68 | return False 69 | else: 70 | return True 71 | 72 | 73 | if __name__ == "__main__": 74 | arguments = sys.argv[1:] 75 | # Database Connection 76 | if "database" in arguments: 77 | import psycopg2 78 | 79 | print("Waiting for database to become available...") 80 | connected = False 81 | while not connected: 82 | connected = database_status() 83 | print("Connection with database established.") 84 | # OpenWISP Dashboard Connection 85 | if "dashboard" in arguments: 86 | print("Waiting for OpenWISP dashboard to become available...") 87 | connected = False 88 | while not connected: 89 | connected = dashboard_status() 90 | print("Connection with OpenWISP dashboard established.") 91 | # Redis Connection 92 | if "redis" in arguments: 93 | import redis 94 | 95 | print("Waiting for redis to become available...") 96 | connected = False 97 | while not connected: 98 | connected = redis_status() 99 | print("Connection with redis established.") 100 | if "uwsgi_status" in arguments: 101 | target = sys.argv[2] 102 | uwsgi_status(target, exit_on_error=True) 103 | -------------------------------------------------------------------------------- /images/common/utils.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import socket 3 | from urllib.parse import urlsplit 4 | 5 | 6 | class UwsgiPacketHeader(ctypes.Structure): 7 | """Represents the uWSGI packet header structure. 8 | 9 | This structure contains three fields: 10 | 11 | - modifier1: An 8-bit modifier. 12 | - datasize: A 16-bit data size. 13 | - modifier2: An 8-bit modifier. 14 | """ 15 | 16 | _pack_ = 1 17 | _fields_ = [ 18 | ("modifier1", ctypes.c_int8), 19 | ("datasize", ctypes.c_int16), 20 | ("modifier2", ctypes.c_int8), 21 | ] 22 | 23 | 24 | class UwsgiVar(object): 25 | """Represents a uWSGI variable structure. 26 | 27 | This structure contains four fields: 28 | 29 | - key_size: A 16-bit size of the key. 30 | - key: A key of size `key_size`. 31 | - val_size: A 16-bit size of the value. 32 | - val: A value of size `val_size`. 33 | """ 34 | 35 | def __new__(self, key_size, key, val_size, val): 36 | class UwsgiVar(ctypes.Structure): 37 | _pack_ = 1 38 | _fields_ = [ 39 | ("key_size", ctypes.c_int16), 40 | ("key", ctypes.c_char * key_size), 41 | ("val_size", ctypes.c_int16), 42 | ("val", ctypes.c_char * val_size), 43 | ] 44 | 45 | return UwsgiVar(key_size, key, val_size, val) 46 | 47 | @classmethod 48 | def from_buffer(cls, buffer, offset=0): 49 | """Create a UwsgiVar instance from a buffer. 50 | 51 | Parameters: 52 | 53 | - buffer (bytes): The buffer containing the uWSGI variable data. 54 | - offset (int, optional): The offset in the buffer where the 55 | - data starts. Defaults to 0. 56 | 57 | Returns: 58 | 59 | - UwsgiVar: The uWSGI variable instance. 60 | """ 61 | key_size = ctypes.c_int16.from_buffer(buffer, offset).value 62 | offset += ctypes.sizeof(ctypes.c_int16) 63 | key = (ctypes.c_char * key_size).from_buffer(buffer, offset).value 64 | offset += ctypes.sizeof(ctypes.c_char * key_size) 65 | val_size = ctypes.c_int16.from_buffer(buffer, offset).value 66 | offset += ctypes.sizeof(ctypes.c_int16) 67 | val = (ctypes.c_char * val_size).from_buffer(buffer, offset).value 68 | 69 | return cls(key_size, key, val_size, val) 70 | 71 | 72 | def pack_uwsgi_vars(var): 73 | """Pack a dictionary of variables into a uWSGI packet format. 74 | 75 | Parameters: 76 | 77 | - var (dict): The dictionary containing key-value pairs. 78 | 79 | Returns: 80 | 81 | - bytes: The packed uWSGI packet. 82 | """ 83 | encoded_vars = [(k.encode("utf-8"), v.encode("utf-8")) for k, v in var.items()] 84 | packed_vars = b"".join( 85 | bytes(UwsgiVar(len(k), k, len(v), v)) for k, v in encoded_vars 86 | ) 87 | packet_header = bytes(UwsgiPacketHeader(0, len(packed_vars), 0)) 88 | return packet_header + packed_vars 89 | 90 | 91 | def parse_addr(addr, default_port=3030): 92 | """Parse an address string or tuple into a host and port. 93 | 94 | Parameters: 95 | 96 | - addr (str, list, tuple, or set): The address to parse. 97 | - default_port (int, optional): The default port to use if none is 98 | - provided. Defaults to 3030. 99 | 100 | Returns: 101 | 102 | - tuple: A tuple containing the host and port. 103 | """ 104 | host = None 105 | port = None 106 | if isinstance(addr, str): 107 | if addr.isdigit(): 108 | port = addr 109 | else: 110 | parts = urlsplit(f"//{addr}") 111 | host = parts.hostname 112 | port = parts.port 113 | elif isinstance(addr, (list, tuple, set)): 114 | host, port = addr 115 | return (host or "127.0.0.1", int(port) if port else default_port) 116 | 117 | 118 | def get_host_from_url(url): 119 | """Extract the host from a URL. 120 | 121 | Parameters: 122 | 123 | - url (str): The URL string. 124 | 125 | Returns: 126 | 127 | - tuple: A tuple containing the host and the remaining URL path. 128 | """ 129 | url = url.split("://")[-1] 130 | 131 | if url and url[0] != "/": 132 | host, _, url = url.partition("/") 133 | return (host, f"/{url}") 134 | 135 | return "", url 136 | 137 | 138 | def ask_uwsgi(uwsgi_addr, var, body="", timeout=0, udp=False): 139 | """Send a request to a uWSGI server and receive the response. 140 | 141 | Parameters: 142 | 143 | - uwsgi_addr (str or tuple): The uWSGI server address. var (dict): 144 | - The dictionary of uWSGI variables. body (str, optional): The body 145 | - of the request. Defaults to ''. timeout (int, optional): The 146 | - timeout for the request. Defaults to 0. udp (bool, optional): 147 | - Whether to use UDP. Defaults to False. 148 | 149 | Returns: 150 | 151 | - str: The response from the uWSGI server. 152 | """ 153 | sock_type = socket.SOCK_DGRAM if udp else socket.SOCK_STREAM 154 | if isinstance(uwsgi_addr, str) and "/" in uwsgi_addr: 155 | addr = uwsgi_addr 156 | s = socket.socket(family=socket.AF_UNIX, type=sock_type) 157 | else: 158 | addr = parse_addr(addr=uwsgi_addr) 159 | s = socket.socket(*socket.getaddrinfo(addr[0], addr[1], 0, sock_type)[0][:2]) 160 | 161 | if timeout: 162 | s.settimeout(timeout) 163 | 164 | if body is None: 165 | body = "" 166 | 167 | s.connect(addr) 168 | s.send(pack_uwsgi_vars(var) + body.encode("utf8")) 169 | response = [] 170 | while 1: 171 | data = s.recv(4096) 172 | if not data: 173 | break 174 | response.append(data) 175 | 176 | s.close() 177 | return b"".join(response).decode("utf8") 178 | 179 | 180 | def uwsgi_curl(uwsgi_addr, method="GET", body="", timeout=0, headers=(), udp=False): 181 | """Send an HTTP-like request to a uWSGI server. 182 | 183 | Parameters: 184 | 185 | - uwsgi_addr (str): The uWSGI server address. method (str, 186 | - optional): The HTTP method to use. Defaults to 'GET'. body (str, 187 | - optional): The body of the request. Defaults to ''. timeout (int, 188 | - optional): The timeout for the request. Defaults to 0. headers 189 | - (tuple, optional): Additional headers to include in the request. 190 | - Defaults to (). udp (bool, optional): Whether to use UDP. Defaults 191 | - to False. 192 | 193 | Returns: 194 | 195 | - str: The response from the uWSGI server. 196 | """ 197 | host, uri = get_host_from_url(uwsgi_addr) 198 | parts_uri = urlsplit(uri) 199 | 200 | if "/" not in uwsgi_addr: 201 | addr = parse_addr(addr=uwsgi_addr) 202 | if not host: 203 | host = addr[0] 204 | port = addr[1] 205 | else: 206 | port = None 207 | 208 | var = { 209 | "SERVER_PROTOCOL": "HTTP/1.1", 210 | "PATH_INFO": parts_uri.path, 211 | "REQUEST_METHOD": method.upper(), 212 | "REQUEST_URI": uri, 213 | "QUERY_STRING": parts_uri.query, 214 | "HTTP_HOST": host, 215 | } 216 | for header in headers or (): 217 | key, _, value = header.partition(":") 218 | var[f"HTTP_{key.strip().upper().replace('-', '_')}"] = value.strip() 219 | var["SERVER_NAME"] = var["HTTP_HOST"] 220 | if port: 221 | var["SERVER_PORT"] = str(port) 222 | 223 | result = ask_uwsgi(uwsgi_addr=host, var=var, body=body, timeout=timeout, udp=udp) 224 | return result 225 | -------------------------------------------------------------------------------- /images/common/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | function init_conf { 4 | default_psql_vars 5 | } 6 | 7 | function default_psql_vars { 8 | # Set database variable values in default PG 9 | # vars to use psql command without passing additional 10 | # arguments. 11 | export PGHOST=$DB_HOST 12 | export PGPORT=$DB_PORT 13 | export PGUSER=$DB_USER 14 | export PGPASSWORD=$DB_PASS 15 | export PGDATABASE=$DB_NAME 16 | export PGSSLMODE=$DB_SSLMODE 17 | export PGSSLCERT=$DB_SSLCERT 18 | export PGSSLKEY=$DB_SSLKEY 19 | export PGSSLROOTCERT=$DB_SSLROOTCERT 20 | } 21 | 22 | function start_uwsgi { 23 | # If a user supplies custom uWSGI configuration, then 24 | # due to lack of write permissions this command will fail. 25 | # Hence, OR (||) operator is used here to continue execution 26 | # of the script. 27 | envsubst uwsgi.ini || true 28 | uwsgi --ini uwsgi.ini 29 | } 30 | 31 | function create_prod_certs { 32 | if [ ! -f /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/privkey.pem ]; then 33 | certbot certonly --standalone --noninteractive --agree-tos \ 34 | --rsa-key-size 4096 \ 35 | --domain ${DASHBOARD_DOMAIN} \ 36 | --email ${CERT_ADMIN_EMAIL} 37 | fi 38 | if [ ! -f /etc/letsencrypt/live/${API_DOMAIN}/privkey.pem ]; then 39 | certbot certonly --standalone --noninteractive --agree-tos \ 40 | --rsa-key-size 4096 \ 41 | --domain ${API_DOMAIN} \ 42 | --email ${CERT_ADMIN_EMAIL} 43 | fi 44 | } 45 | 46 | function create_dev_certs { 47 | # Ensure required directories exist 48 | mkdir -p /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/ 49 | mkdir -p /etc/letsencrypt/live/${API_DOMAIN}/ 50 | # Create self-signed certificates 51 | if [ ! -f /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/privkey.pem ]; then 52 | openssl req -x509 -newkey rsa:4096 \ 53 | -keyout /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/privkey.pem \ 54 | -out /etc/letsencrypt/live/${DASHBOARD_DOMAIN}/fullchain.pem \ 55 | -days 365 -nodes -subj '/CN=OpenWISP' 56 | fi 57 | if [ ! -f /etc/letsencrypt/live/${API_DOMAIN}/privkey.pem ]; then 58 | openssl req -x509 -newkey rsa:4096 \ 59 | -keyout /etc/letsencrypt/live/${API_DOMAIN}/privkey.pem \ 60 | -out /etc/letsencrypt/live/${API_DOMAIN}/fullchain.pem \ 61 | -days 365 -nodes -subj '/CN=OpenWISP' 62 | fi 63 | } 64 | 65 | function nginx_dev { 66 | envsubst_create_config /etc/nginx/openwisp.ssl.template.conf https DOMAIN 67 | ssl_http_behaviour 68 | create_dev_certs 69 | CMD="source /etc/nginx/utils.sh && create_dev_certs && nginx -s reload" 70 | echo "0 3 1 1 * $CMD &>> /etc/nginx/log/crontab.log" | crontab - 71 | nginx -g 'daemon off;' 72 | } 73 | 74 | function nginx_prod { 75 | create_prod_certs 76 | ssl_http_behaviour 77 | envsubst_create_config /etc/nginx/openwisp.ssl.template.conf https DOMAIN 78 | CMD="certbot --nginx renew && nginx -s reload" 79 | echo "0 3 * * 7 ${CMD} &>> /etc/nginx/log/crontab.log" | crontab - 80 | } 81 | 82 | function wait_nginx_services { 83 | # Wait for nginx to start up and then check 84 | # if the openwisp-dashboard is reachable. 85 | echo "Waiting for dashboard to become available..." 86 | # Make fault tolerant to ensure connection 87 | # error report by `wget` is received. 88 | set +e 89 | while :; do 90 | wget -qS ${DASHBOARD_INTERNAL}/admin/login/ 2>&1 | grep -q "200 OK" 91 | if [[ $? = "0" ]]; then 92 | FAILURE=0 93 | echo "Connection with dashboard established." 94 | break 95 | fi 96 | sleep 5 97 | done 98 | set -e # Restore previous error setting. 99 | } 100 | 101 | function ssl_http_behaviour { 102 | if [ "$NGINX_HTTP_ALLOW" == "True" ]; then 103 | envsubst_create_config /etc/nginx/openwisp.template.conf http DOMAIN 104 | else 105 | envsubst /etc/nginx/conf.d/openwisp.http.conf 106 | fi 107 | } 108 | 109 | function envsubst_create_config { 110 | # Creates nginx configurations files for dashboard 111 | # and api instances. 112 | for application in DASHBOARD API; do 113 | eval export APP_SERVICE=\$${application}_APP_SERVICE 114 | eval export APP_PORT=\$${application}_APP_PORT 115 | eval export DOMAIN=\$${application}_${3} 116 | eval export ROOT_DOMAIN=$(python3 get_domain.py) 117 | application=$(echo "$application" | tr "[:upper:]" "[:lower:]") 118 | envsubst <${1} >/etc/nginx/conf.d/${application}.${2}.conf 119 | done 120 | } 121 | 122 | function postfix_config { 123 | # This function is used to configure the 124 | # postfix instance. 125 | 126 | mkdir -p /var/spool/postfix/ /var/spool/postfix/pid /var/lib/postfix/ 127 | chmod 755 /var/spool/postfix/ /var/spool/postfix/pid /var/lib/postfix/ 128 | rm -rf /etc/aliases /etc/postfix/generic /etc/allowed_senders /etc/postfix/main.cf /var/run/rsyslogd.pid 129 | touch /etc/aliases /etc/postfix/generic /etc/allowed_senders /etc/postfix/main.cf 130 | # Create ssl-certs 131 | if [ ! -f /etc/ssl/mail/openwisp.mail.key ]; then 132 | openssl req -new -nodes -x509 -subj '/CN=openwisp-postfix' -days 3650 -keyout /etc/ssl/mail/openwisp.mail.key -out /etc/ssl/mail/openwisp.mail.crt -extensions v3_ca 133 | fi 134 | 135 | # Disable SMTPUTF8, because libraries (ICU) are missing in alpine 136 | postconf -e smtputf8_enable=no 137 | 138 | # Configure posfix 139 | postconf -e biff=no 140 | postconf -e append_dot_mydomain=no 141 | postconf -e readme_directory=no 142 | 143 | postconf -e smtpd_use_tls=yes 144 | postconf -e smtpd_tls_cert_file=/etc/ssl/mail/openwisp.mail.crt 145 | postconf -e smtpd_tls_key_file=/etc/ssl/mail/openwisp.mail.key 146 | postconf -e smtpd_tls_session_cache_database=btree:/var/lib/postfix/smtpd_scache 147 | postconf -e smtp_tls_session_cache_database=btree:/var/lib/postfix/smtp_scache 148 | 149 | postconf -e myhostname="$POSTFIX_MYHOSTNAME" 150 | postconf -e myorigin='$myhostname' 151 | postconf -e alias_maps=lmdb:/etc/aliases 152 | postconf -e smtp_generic_maps=lmdb:/etc/postfix/generic 153 | postconf -e alias_database=lmdb:/etc/aliases 154 | postconf -e mydestination="$POSTFIX_DESTINATION" 155 | 156 | postconf -e mynetworks="$POSTFIX_MYNETWORKS" 157 | postconf -e message_size_limit="$POSTFIX_MESSAGE_SIZE_LIMIT" 158 | postconf -e mailbox_size_limit=0 159 | postconf -e recipient_delimiter=+ 160 | postconf -e inet_protocols=all 161 | 162 | postconf -e bounce_queue_lifetime=1h 163 | postconf -e maximal_queue_lifetime=1h 164 | postconf -e maximal_backoff_time=15m 165 | postconf -e minimal_backoff_time=5m 166 | postconf -e queue_run_delay=5m 167 | 168 | if [ "$POSTFIX_ALLOWED_SENDER_DOMAINS" != 'null' ]; then 169 | for i in $POSTFIX_ALLOWED_SENDER_DOMAINS; do 170 | echo -e "$i\tOK" >>/etc/allowed_senders 171 | done 172 | postmap /etc/allowed_senders 173 | postconf -e "smtpd_restriction_classes=allowed_domains_only" 174 | postconf -e "allowed_domains_only=permit_mynetworks, reject_non_fqdn_sender reject" 175 | postconf -e "smtpd_recipient_restrictions=reject_non_fqdn_recipient, check_sender_access lmdb:/etc/allowed_senders,permit_sasl_authenticated, reject_unauth_destination" 176 | postconf -e "smtpd_relay_restrictions=permit" 177 | fi 178 | 179 | if [ "$POSTFIX_RELAYHOST" != 'null' ]; then 180 | postconf -e "relayhost=$POSTFIX_RELAYHOST" 181 | postconf -e smtp_tls_CAfile=/etc/ssl/mail/openwisp.mail.crt 182 | if [ "$POSTFIX_RELAYHOST_USERNAME" != 'null' ] && [ "$POSTFIX_RELAYHOST_PASSWORD" != 'null' ]; then 183 | echo "$POSTFIX_RELAYHOST $POSTFIX_RELAYHOST_USERNAME:$POSTFIX_RELAYHOST_PASSWORD" >>/etc/postfix/sasl_passwd 184 | postmap lmdb:/etc/postfix/sasl_passwd 185 | postconf -e "smtp_sasl_auth_enable=yes" 186 | postconf -e "smtp_sasl_password_maps=lmdb:/etc/postfix/sasl_passwd" 187 | postconf -e "smtp_sasl_security_options=noanonymous" 188 | postconf -e "smtp_sasl_tls_security_options=noanonymous" 189 | fi 190 | postconf -e smtp_tls_security_level="$POSTFIX_RELAYHOST_TLS_LEVEL" 191 | fi 192 | 193 | if [ "$POSTFIX_DEBUG_MYNETWORKS" != 'null' ]; then 194 | postconf -e debug_peer_level=10 195 | postconf -e debug_peer_list="$POSTFIX_DEBUG_MYNETWORKS" 196 | fi 197 | 198 | postmap /etc/postfix/generic 199 | postmap /etc/aliases 200 | newaliases 201 | } 202 | 203 | get_redis_value() { 204 | local key="$1" 205 | echo -en "GET $key\r\n" | nc redis 6379 | awk 'NR==2 {gsub(/\r/, ""); print}' 206 | } 207 | 208 | function openvpn_preconfig { 209 | mkdir -p /dev/net 210 | if [ ! -c /dev/net/tun ]; then 211 | mknod /dev/net/tun c 10 200 212 | fi 213 | ip -6 route show default 2>/dev/null 214 | if [ $? = 0 ]; then 215 | echo "Enabling IPv6 Forwarding" 216 | sysctl -w net.ipv6.conf.all.disable_ipv6=0 || echo "Failed to enable IPv6 support" 217 | sysctl -w net.ipv6.conf.default.forwarding=1 || echo "Failed to enable IPv6 Forwarding default" 218 | sysctl -w net.ipv6.conf.all.forwarding=1 || echo "Failed to enable IPv6 Forwarding" 219 | fi 220 | } 221 | 222 | function openvpn_config { 223 | # Fectch UUID and Key of the default VPN only if they 224 | # are not already set. The user may override the UUID and Key 225 | # by setting them in the environment variables to use deploy 226 | # a different VPN server. 227 | if [ -z "$UUID" ]; then 228 | export UUID=$(get_redis_value "openwisp_default_vpn_uuid") 229 | export KEY=$(get_redis_value "openwisp_default_vpn_key") 230 | export CA_UUID=$(get_redis_value "openwisp_default_vpn_ca_uuid") 231 | fi 232 | } 233 | 234 | function openvpn_config_checksum { 235 | export OFILE=$(curl --silent --insecure \ 236 | ${API_INTERNAL}/controller/vpn/checksum/$UUID/?key=$KEY) 237 | export NFILE=$(cat checksum) 238 | } 239 | 240 | function openvpn_config_download { 241 | curl --silent --retry 10 --retry-delay 5 --retry-max-time 300\ 242 | --insecure --output vpn.tar.gz \ 243 | ${API_INTERNAL}/controller/vpn/download-config/$UUID/?key=$KEY 244 | curl --silent --insecure --output checksum \ 245 | ${API_INTERNAL}/controller/vpn/checksum/$UUID/?key=$KEY 246 | tar xzf vpn.tar.gz 247 | chmod 600 *.pem 248 | } 249 | 250 | function crl_download { 251 | curl --silent --insecure --output revoked.crl \ 252 | ${DASHBOARD_INTERNAL}/admin/pki/ca/x509/ca/${CA_UUID}.crl 253 | } 254 | 255 | function init_send_network_topology { 256 | if [ -z "$TOPOLOGY_UUID" ]; then 257 | export TOPOLOGY_UUID=$(get_redis_value "default_openvpn_topology_uuid") 258 | export TOPOLOGY_KEY=$(get_redis_value "default_openvpn_topology_key") 259 | fi 260 | ( 261 | crontab -l 262 | echo "*/$TOPLOGY_UPDATE_INTERVAL * * * * TOPOLOGY_UUID=$TOPOLOGY_UUID TOPOLOGY_KEY=$TOPOLOGY_KEY sh /send-topology.sh" 263 | ) | crontab - 264 | } 265 | -------------------------------------------------------------------------------- /images/common/uwsgi.conf.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | chdir=/opt/openwisp 3 | module=openwisp.wsgi:application 4 | master=True 5 | pidfile=/opt/openwisp/uwsgi.pid 6 | socket=0.0.0.0:${CONTAINER_PORT} 7 | processes=${UWSGI_PROCESSES} 8 | threads=${UWSGI_THREADS} 9 | listen=${UWSGI_LISTEN} 10 | harakiri=20 11 | max-requests=5000 12 | vacuum=True 13 | enable-threads=True 14 | env=HTTPS=on 15 | buffer-size=8192 16 | log-format = [${HOSTNAME}] - pid: %(pid) %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) %(uri) => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core)) 17 | ignore-sigpipe 18 | ignore-write-errors 19 | disable-write-exception 20 | -------------------------------------------------------------------------------- /images/openwisp_api/Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint ignore=DL3007 2 | FROM openwisp/openwisp-base:latest 3 | 4 | WORKDIR /opt/openwisp/ 5 | 6 | COPY --chown=openwisp:root ./openwisp_api/urls.py \ 7 | ./openwisp_api/module_settings.py \ 8 | /opt/openwisp/openwisp/ 9 | 10 | CMD ["bash", "init_command.sh"] 11 | 12 | ARG API_APP_PORT=8001 13 | ENV MODULE_NAME=api \ 14 | CONTAINER_PORT=$API_APP_PORT \ 15 | DASHBOARD_APP_PORT=8000 16 | 17 | EXPOSE $API_APP_PORT 18 | -------------------------------------------------------------------------------- /images/openwisp_api/module_settings.py: -------------------------------------------------------------------------------- 1 | INSTALLED_APPS = [ 2 | "django.contrib.auth", 3 | "django.contrib.contenttypes", 4 | "django.contrib.sessions", 5 | "django.contrib.messages", 6 | "django.contrib.staticfiles", 7 | "django.contrib.gis", 8 | "django.contrib.humanize", 9 | # all-auth 10 | "django.contrib.sites", 11 | # overrides allauth templates 12 | # must precede allauth 13 | "openwisp_users.accounts", 14 | "allauth", 15 | "allauth.account", 16 | "allauth.socialaccount", 17 | "django_extensions", 18 | # openwisp modules 19 | "openwisp_users", 20 | # openwisp-controller 21 | "openwisp_controller.pki", 22 | "openwisp_controller.config", 23 | "openwisp_controller.geo", 24 | "openwisp_controller.connection", 25 | "openwisp_controller.subnet_division", 26 | # openwisp-monitoring 27 | "openwisp_monitoring.monitoring", 28 | "openwisp_monitoring.device", 29 | "openwisp_monitoring.check", 30 | "nested_admin", 31 | # openwisp-notification 32 | "openwisp_notifications", 33 | # openwisp-ipam 34 | "openwisp_ipam", 35 | # openwisp-network-topology 36 | "openwisp_network_topology", 37 | # openwisp-firmware-upgrader 38 | "openwisp_firmware_upgrader", 39 | # openwisp radius 40 | "dj_rest_auth", 41 | "dj_rest_auth.registration", 42 | "openwisp_radius", 43 | # admin 44 | "openwisp_utils.admin_theme", 45 | "django.contrib.admin", 46 | "django.forms", 47 | # other dependencies 48 | "sortedm2m", 49 | "reversion", 50 | "leaflet", 51 | # rest framework 52 | "rest_framework", 53 | "rest_framework_gis", 54 | "rest_framework.authtoken", 55 | "django_filters", 56 | # social login 57 | "allauth.socialaccount.providers.facebook", 58 | "allauth.socialaccount.providers.google", 59 | # other dependencies 60 | "flat_json_widget", 61 | "private_storage", 62 | "drf_yasg", 63 | "import_export", 64 | "admin_auto_filters", 65 | "channels", 66 | "corsheaders", 67 | ] 68 | 69 | EXTENDED_APPS = [ 70 | "django_x509", 71 | "django_loci", 72 | ] 73 | 74 | LOGIN_REDIRECT_URL = "account_change_password" 75 | ACCOUNT_LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL 76 | -------------------------------------------------------------------------------- /images/openwisp_api/urls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from openwisp.utils import env_bool, openwisp_controller_urls 6 | from openwisp_users.api.urls import get_api_urls as users_api 7 | 8 | urlpatterns = openwisp_controller_urls() + [ 9 | path("admin/", admin.site.urls), 10 | path("api/v1/", include((users_api(), "users"))), 11 | path("api/v1/", include("openwisp_utils.api.urls")), 12 | ] 13 | 14 | if env_bool(os.environ["USE_OPENWISP_TOPOLOGY"]): 15 | from openwisp_network_topology.api import views 16 | from openwisp_network_topology.utils import get_api_urls as topology_api 17 | 18 | urlpatterns += [path("api/v1/", include(topology_api(views)))] 19 | 20 | if env_bool(os.environ["USE_OPENWISP_FIRMWARE"]): 21 | from openwisp_firmware_upgrader.private_storage.urls import ( 22 | urlpatterns as fw_private_storage_urls, 23 | ) 24 | 25 | urlpatterns += [ 26 | path("", include("openwisp_firmware_upgrader.urls")), 27 | path( 28 | "", 29 | include((fw_private_storage_urls, "firmware"), namespace="firmware"), 30 | ), 31 | ] 32 | 33 | if env_bool(os.environ["USE_OPENWISP_MONITORING"]): 34 | urlpatterns += [ 35 | path("", include("openwisp_monitoring.urls")), 36 | ] 37 | 38 | if env_bool(os.environ["USE_OPENWISP_RADIUS"]): 39 | urlpatterns += [path("", include("openwisp_radius.urls"))] 40 | -------------------------------------------------------------------------------- /images/openwisp_base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-slim-bullseye AS system 2 | 3 | # System requirements: 4 | # 1. gettext: Required by envsubst used in scripts 5 | # 2. openwisp-radius/weasyprint: libcairo2 libpangocairo-1.0 6 | # 3. openwisp-monitoring: fping gdal-bin 7 | # hadolint ignore=DL3008 8 | RUN apt-get update && \ 9 | apt-get install --yes --no-install-recommends \ 10 | libcairo2 apt-utils libpangocairo-1.0-0 \ 11 | gdal-bin gettext fping openssh-client && \ 12 | rm -rf /var/lib/apt/lists/* /root/.cache/pip/* /tmp/* 13 | # hadolint ignore=DL3008, DL3009 14 | RUN apt-get update && \ 15 | apt-get install --yes --no-install-recommends \ 16 | libpq-dev libjpeg-dev libffi-dev python3-dev \ 17 | python3-pip libxml2-dev libxslt1-dev zlib1g-dev g++ procps 18 | 19 | RUN useradd --system --password '' --create-home --shell /bin/bash \ 20 | --gid root --uid 1001 openwisp 21 | USER openwisp:root 22 | 23 | FROM system AS openwisp_python 24 | 25 | ENV PATH="${PATH}:/home/openwisp/.local/bin" 26 | ENV PYTHONPATH=/home/openwisp/.local/lib/python3.10/site-packages 27 | 28 | RUN pip install --no-cache-dir --user --upgrade pip~=24.1.2 setuptools~=70.3.0 wheel~=0.43.0 29 | ARG OPENWISP_MONITORING_SOURCE="https://github.com/openwisp/openwisp-monitoring/tarball/1.2" 30 | # hadolint ignore=DL3013 31 | RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_MONITORING_SOURCE} 32 | ARG OPENWISP_FIRMWARE_SOURCE="https://github.com/openwisp/openwisp-firmware-upgrader/tarball/1.2" 33 | # hadolint ignore=DL3013 34 | RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_FIRMWARE_SOURCE} 35 | ARG OPENWISP_TOPOLOGY_SOURCE="https://github.com/openwisp/openwisp-network-topology/tarball/1.2" 36 | # hadolint ignore=DL3013 37 | RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_TOPOLOGY_SOURCE} 38 | ARG OPENWISP_RADIUS_SOURCE="https://github.com/openwisp/openwisp-radius/tarball/1.2" 39 | # hadolint ignore=DL3013 40 | RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_RADIUS_SOURCE} 41 | 42 | # here we try to install custom versions of the modules only if the 43 | # supplied argument does not equal the default value, because 44 | # otherwise these modules will have already been installed above 45 | ARG OPENWISP_IPAM_SOURCE=default 46 | # hadolint ignore=DL3013 47 | RUN if [ "$OPENWISP_IPAM_SOURCE" != "default" ] ; then \ 48 | pip install --no-cache-dir --user --upgrade ${OPENWISP_IPAM_SOURCE}; \ 49 | fi 50 | ARG OPENWISP_CONTROLLER_SOURCE=default 51 | # hadolint ignore=DL3013 52 | RUN if [ "$OPENWISP_CONTROLLER_SOURCE" != "default" ] ; then \ 53 | pip install --no-cache-dir --user --upgrade ${OPENWISP_CONTROLLER_SOURCE}; \ 54 | fi 55 | ARG OPENWISP_NOTIFICATION_SOURCE=default 56 | # hadolint ignore=DL3013 57 | RUN if [ "$OPENWISP_NOTIFICATION_SOURCE" != "default" ] ; then \ 58 | pip install --no-cache-dir --user --upgrade ${OPENWISP_NOTIFICATION_SOURCE}; \ 59 | fi 60 | ARG OPENWISP_USERS_SOURCE=default 61 | # hadolint ignore=DL3013 62 | RUN if [ "$OPENWISP_USERS_SOURCE" != "default" ] ; then \ 63 | pip install --no-cache-dir --user --upgrade --force-reinstall ${OPENWISP_USERS_SOURCE}; \ 64 | fi 65 | ARG OPENWISP_UTILS_SOURCE=default 66 | # hadolint ignore=DL3013 67 | RUN if [ "$OPENWISP_UTILS_SOURCE" != "default" ] ; then \ 68 | pip install --no-cache-dir --user --upgrade --force-reinstall "${OPENWISP_UTILS_SOURCE}"; \ 69 | fi 70 | ARG DJANGO_X509_SOURCE=default 71 | # hadolint ignore=DL3013 72 | RUN if [ "$DJANGO_X509_SOURCE" != "default" ]; then \ 73 | pip install --no-cache-dir --user --upgrade --force-reinstall ${DJANGO_X509_SOURCE}; \ 74 | fi 75 | 76 | ARG DJANGO_SOURCE=django~=5.2.0 77 | # hadolint ignore=DL3013 78 | RUN pip install --no-cache-dir --user --upgrade ${DJANGO_SOURCE} 79 | 80 | COPY ./openwisp_base/requirements.txt /tmp/openwisp-deploy-requirements.txt 81 | RUN pip install --no-cache-dir --user --upgrade -r /tmp/openwisp-deploy-requirements.txt && \ 82 | pip install --no-cache-dir --upgrade urllib3~=2.2.1 83 | 84 | FROM system 85 | 86 | COPY --from=openwisp_python --chown=openwisp:root /home/openwisp/.local/ /usr/local 87 | COPY --chown=openwisp:root ./common/ /opt/openwisp/ 88 | RUN mkdir /opt/openwisp/static && \ 89 | mkdir /opt/openwisp/media && \ 90 | mkdir /opt/openwisp/private && \ 91 | mkdir /opt/openwisp/logs && \ 92 | mkdir /home/openwisp/.ssh && \ 93 | chown -R openwisp:root /opt/openwisp && \ 94 | chown -R openwisp:root /home/openwisp/.ssh 95 | # Maintain backward compatibility with code written for ansible-openwisp2 96 | RUN ln -s /opt/openwisp/openwisp /opt/openwisp/openwisp2 97 | 98 | ENV DASHBOARD_APP_SERVICE=dashboard \ 99 | PYTHONUNBUFFERED=1 \ 100 | TZ=UTC \ 101 | DEBUG_MODE=False \ 102 | REDIS_HOST=redis \ 103 | REDIS_PORT=6379 \ 104 | REDIS_PASS= \ 105 | DB_ENGINE=django.contrib.gis.db.backends.postgis \ 106 | DB_NAME=openwisp_db \ 107 | DB_USER=admin \ 108 | DB_PASS=admin \ 109 | DB_HOST=postgres \ 110 | DB_PORT=5432 \ 111 | DB_SSLMODE=disable \ 112 | DB_SSLKEY=None \ 113 | DB_SSLCERT=None \ 114 | DB_SSLROOTCERT=None \ 115 | DB_OPTIONS={} \ 116 | INFLUXDB_USER=admin \ 117 | INFLUXDB_PASS=admin \ 118 | INFLUXDB_NAME=openwisp \ 119 | INFLUXDB_HOST=influxdb \ 120 | INFLUXDB_PORT=8086 \ 121 | INFLUXDB_DEFAULT_RETENTION_POLICY=26280h0m0s \ 122 | EMAIL_BACKEND=djcelery_email.backends.CeleryEmailBackend \ 123 | EMAIL_HOST=postfix \ 124 | EMAIL_HOST_PORT=25 \ 125 | EMAIL_HOST_USER="" \ 126 | EMAIL_HOST_PASSWORD="" \ 127 | EMAIL_HOST_TLS=False \ 128 | EMAIL_TIMEOUT=10 \ 129 | EMAIL_DJANGO_DEFAULT=example@example.org \ 130 | DJANGO_LOG_LEVEL=ERROR \ 131 | DJANGO_LANGUAGE_CODE=en-gb \ 132 | OPENWISP_RADIUS_FREERADIUS_ALLOWED_HOSTS=172.18.0.0/16 \ 133 | DJANGO_X509_DEFAULT_CERT_VALIDITY=1825 \ 134 | DJANGO_X509_DEFAULT_CA_VALIDITY=3650 \ 135 | DJANGO_SECRET_KEY=DEFAULT_BAD_KEY \ 136 | DJANGO_CORS_HOSTS=http://localhost \ 137 | DJANGO_SENTRY_DSN="" \ 138 | DJANGO_LEAFET_CENTER_X_AXIS=0 \ 139 | DJANGO_LEAFET_CENTER_Y_AXIS=0 \ 140 | DJANGO_LEAFET_ZOOM=1 \ 141 | # Common Nginx configurations 142 | NGINX_CLIENT_BODY_SIZE=30 \ 143 | DASHBOARD_APP_PORT=8000 \ 144 | API_APP_PORT=8001 \ 145 | WEBSOCKET_APP_PORT=8002 \ 146 | DASHBOARD_INTERNAL=dashboard.internal \ 147 | API_INTERNAL=api.internal \ 148 | # SSH Credentials Configurations 149 | SSH_PRIVATE_KEY_PATH=/home/openwisp/.ssh/id_ed25519 \ 150 | SSH_PUBLIC_KEY_PATH=/home/openwisp/.ssh/id_ed25519.pub \ 151 | # VPN Configurations 152 | VPN_DOMAIN=openvpn.example.com \ 153 | VPN_NAME=default \ 154 | VPN_CLIENT_NAME=default-management-vpn \ 155 | X509_NAME_CA=default \ 156 | X509_NAME_CERT=default \ 157 | X509_COUNTRY_CODE=IN \ 158 | X509_STATE=Delhi \ 159 | X509_CITY="New Delhi" \ 160 | X509_ORGANIZATION_NAME=OpenWISP \ 161 | X509_ORGANIZATION_UNIT_NAME=OpenWISP \ 162 | X509_EMAIL=certificate@example.com \ 163 | X509_COMMON_NAME=OpenWISP \ 164 | # Modules Enabled 165 | USE_OPENWISP_RADIUS=True \ 166 | USE_OPENWISP_TOPOLOGY=True \ 167 | USE_OPENWISP_FIRMWARE=True \ 168 | USE_OPENWISP_MONITORING=True \ 169 | USE_OPENWISP_CELERY_TASK_ROUTES_DEFAULTS=True \ 170 | # Celery-beat Configurations 171 | CRON_DELETE_OLD_RADACCT=365 \ 172 | CRON_DELETE_OLD_POSTAUTH=365 \ 173 | CRON_CLEANUP_STALE_RADACCT=365 \ 174 | CRON_DELETE_OLD_RADIUSBATCH_USERS=365 \ 175 | METRIC_COLLECTION=True 176 | -------------------------------------------------------------------------------- /images/openwisp_base/requirements.txt: -------------------------------------------------------------------------------- 1 | channels_redis 2 | service_identity 3 | django-redis 4 | psycopg2 5 | sentry-sdk 6 | supervisor~=4.2 # allows 4.x and > 4.2 7 | django-cors-headers~=4.7 8 | django-pipeline~=4.0 9 | uwsgi~=2.0.29 10 | django-celery-email~=3.0.0 11 | tldextract~=5.3.0 12 | # these add support for object storage 13 | # (eg: Amazon S3, GCS) 14 | django-storages~=1.14.6 15 | boto3~=1.38.28 16 | -------------------------------------------------------------------------------- /images/openwisp_dashboard/Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint ignore=DL3007 2 | FROM openwisp/openwisp-base:latest 3 | WORKDIR /opt/openwisp/ 4 | 5 | # Location: /opt/openwisp/ 6 | COPY --chown=openwisp:root ./openwisp_dashboard/load_init_data.py \ 7 | ./openwisp_dashboard/openvpn.json \ 8 | /opt/openwisp/ 9 | # Location: /opt/openwisp/openwisp/ 10 | COPY --chown=openwisp:root ./openwisp_dashboard/module_settings.py \ 11 | ./openwisp_dashboard/urls.py \ 12 | /opt/openwisp/openwisp/ 13 | 14 | CMD ["bash", "init_command.sh"] 15 | 16 | ARG DASHBOARD_APP_PORT=8000 17 | ENV MODULE_NAME=dashboard \ 18 | OPENWISP_GEOCODING_CHECK=True \ 19 | OPENWISP_CELERY_COMMAND_FLAGS=--concurrency=1 \ 20 | USE_OPENWISP_CELERY_NETWORK=True \ 21 | OPENWISP_CELERY_NETWORK_COMMAND_FLAGS=--concurrency=1 \ 22 | USE_OPENWISP_CELERY_MONITORING=True \ 23 | OPENWISP_CELERY_MONITORING_COMMAND_FLAGS=--concurrency=1 \ 24 | OPENWISP_CELERY_MONITORING_CHECKS_COMMAND_FLAGS=--concurrency=1 \ 25 | USE_OPENWISP_CELERY_FIRMWARE=True \ 26 | OPENWISP_CELERY_FIRMWARE_COMMAND_FLAGS=--concurrency=1 \ 27 | CONTAINER_PORT=$DASHBOARD_APP_PORT 28 | 29 | EXPOSE $DASHBOARD_APP_PORT 30 | -------------------------------------------------------------------------------- /images/openwisp_dashboard/load_init_data.py: -------------------------------------------------------------------------------- 1 | """Load initial data before starting the server. 2 | 3 | - Create superuser `admin`. 4 | - Create default CA 5 | - Create default Cert 6 | - Create default VPN 7 | - Create default VPN Client Template 8 | - Create default Credentials 9 | - Create SSH Key template 10 | """ 11 | 12 | import json 13 | import os 14 | 15 | import django 16 | import redis 17 | import redis.exceptions 18 | from openwisp.utils import env_bool 19 | 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp.settings") 21 | django.setup() 22 | from django.conf import settings # noqa 23 | 24 | 25 | def create_admin(): 26 | """Creates superuser `admin` if it does not exist.""" 27 | User.objects.filter(is_superuser=True).exists() or User.objects.create_superuser( 28 | "admin", "admin@example.com", "admin" 29 | ) 30 | 31 | 32 | def create_default_ca(): 33 | """Create default certificate authority.""" 34 | ca_name = os.environ["X509_NAME_CA"] 35 | if Ca.objects.filter(name=ca_name).exists(): 36 | return Ca.objects.get(name=ca_name) 37 | 38 | ca = Ca( 39 | name=ca_name, 40 | country_code=os.environ["X509_COUNTRY_CODE"], 41 | state=os.environ["X509_STATE"], 42 | city=os.environ["X509_CITY"], 43 | organization_name=os.environ["X509_ORGANIZATION_NAME"], 44 | organizational_unit_name=os.environ["X509_ORGANIZATION_UNIT_NAME"], 45 | email=os.environ["X509_EMAIL"], 46 | common_name=os.environ["X509_COMMON_NAME"], 47 | notes=( 48 | "This CA was created during the setup, it is used for " 49 | "the default management VPN. Please do not rename it." 50 | ), 51 | ) 52 | ca.full_clean() 53 | ca.save() 54 | return ca 55 | 56 | 57 | def create_default_cert(ca): 58 | """Creates default certificate.""" 59 | cert_name = os.environ["X509_NAME_CERT"] 60 | if Cert.objects.filter(name=cert_name).exists(): 61 | return Cert.objects.get(name=cert_name) 62 | 63 | cert = Cert( 64 | ca=ca, 65 | name=cert_name, 66 | country_code=os.environ["X509_COUNTRY_CODE"], 67 | state=os.environ["X509_STATE"], 68 | city=os.environ["X509_CITY"], 69 | organization_name=os.environ["X509_ORGANIZATION_NAME"], 70 | organizational_unit_name=os.environ["X509_ORGANIZATION_UNIT_NAME"], 71 | email=os.environ["X509_EMAIL"], 72 | common_name=os.environ["X509_COMMON_NAME"], 73 | notes=( 74 | "This certificate was created during the setup. " 75 | "It is used for the default management VPN. " 76 | "Please do not rename it." 77 | ), 78 | ) 79 | cert.full_clean() 80 | cert.save() 81 | return cert 82 | 83 | 84 | def create_default_vpn(ca, cert): 85 | """Creates default vpn.""" 86 | vpn_name = os.environ["VPN_NAME"] 87 | if Vpn.objects.exists(): 88 | try: 89 | vpn = Vpn.objects.get(name=vpn_name) 90 | except Vpn.DoesNotExist: 91 | # The VPN name might be changed by the user, 92 | # in this scenario, return the first VPN object. 93 | vpn = Vpn.objects.first() 94 | if redis_client.get("openwisp_default_vpn_uuid"): 95 | # The VPN UUID and key has already been set in Redis. 96 | return vpn 97 | else: 98 | vpn = Vpn( 99 | ca=ca, 100 | cert=cert, 101 | name=vpn_name, 102 | notes=( 103 | "This is the default management VPN created during setup, " 104 | "you may modify these settings and they will soon reflect " 105 | "in your OpenVPN Server instance." 106 | ), 107 | host=os.environ["VPN_DOMAIN"], 108 | backend="openwisp_controller.vpn_backends.OpenVpn", 109 | ) 110 | with open("openvpn.json", "r") as json_file: 111 | vpn.config = json.load(json_file) 112 | vpn.full_clean() 113 | vpn.save() 114 | 115 | redis_client.set("openwisp_default_vpn_uuid", str(vpn.id), ex=None) 116 | redis_client.set("openwisp_default_vpn_key", str(vpn.key), ex=None) 117 | redis_client.set("openwisp_default_vpn_ca_uuid", str(ca.id), ex=None) 118 | return vpn 119 | 120 | 121 | def create_default_vpn_template(vpn): 122 | """Creates default vpn client template.""" 123 | template_name = os.environ["VPN_CLIENT_NAME"] 124 | if Template.objects.filter(vpn=vpn).exists(): 125 | return Template.objects.get(vpn=vpn) 126 | 127 | template = Template.objects.create( 128 | auto_cert=True, 129 | name=template_name, 130 | type="vpn", 131 | tags="Management, VPN", 132 | backend="netjsonconfig.OpenWrt", 133 | vpn=vpn, 134 | default=True, 135 | ) 136 | template.full_clean() 137 | template.save() 138 | return template 139 | 140 | 141 | def create_default_credentials(): 142 | private_key_filepath = os.environ["SSH_PRIVATE_KEY_PATH"] 143 | if Credentials.objects.exists(): 144 | return 145 | try: 146 | with open(private_key_filepath, "r") as file: 147 | ssh_private_key = file.read() 148 | except FileNotFoundError: 149 | raise Exception( 150 | "Failed to create default credentials:" 151 | f" SSH private key not found at {private_key_filepath}" 152 | ) 153 | credentials = Credentials( 154 | connector="openwisp_controller.connection.connectors.ssh.Ssh", 155 | name="OpenWISP Default", 156 | auto_add=True, 157 | params={"username": "root", "key": ssh_private_key}, 158 | ) 159 | credentials.full_clean() 160 | credentials.save() 161 | return credentials 162 | 163 | 164 | def create_ssh_key_template(): 165 | if Template.objects.filter( 166 | default=True, config__contains="/etc/dropbear/authorized_keys" 167 | ).exists(): 168 | return Template.objects.filter( 169 | default=True, config__contains="/etc/dropbear/authorized_keys" 170 | ).first() 171 | public_key_filepath = os.environ["SSH_PUBLIC_KEY_PATH"] 172 | try: 173 | with open(public_key_filepath, "r") as file: 174 | ssh_public_key = file.read() 175 | except FileNotFoundError: 176 | raise Exception( 177 | "Failed to default SSH Template:" 178 | f" SSH public key not found at {public_key_filepath}" 179 | ) 180 | template = Template( 181 | name="SSH Keys", 182 | default=True, 183 | backend="netjsonconfig.OpenWrt", 184 | config={ 185 | "files": [ 186 | { 187 | "path": "/etc/dropbear/authorized_keys", 188 | "mode": "0644", 189 | "contents": ssh_public_key, 190 | }, 191 | ] 192 | }, 193 | ) 194 | template.full_clean() 195 | template.save() 196 | return template 197 | 198 | 199 | def create_default_topology(vpn): 200 | """Creates Topology object for the default VPN.""" 201 | if vpn.backend == "openwisp_controller.vpn_backends.OpenVpn": 202 | parser = "netdiff.OpenvpnParser" 203 | topology_label = f"{vpn.name} ({vpn.get_backend_display()})" 204 | if Topology.objects.exists(): 205 | try: 206 | topology = Topology.objects.get(label=topology_label) 207 | except Topology.DoesNotExist: 208 | topology = Topology.objects.first() 209 | if redis_client.get("default_openvpn_topology_uuid"): 210 | # The Topology UUID and key has already been set in Redis. 211 | return topology 212 | else: 213 | topology = Topology( 214 | label=topology_label, 215 | parser=parser, 216 | strategy="receive", 217 | ) 218 | topology.full_clean() 219 | topology.save() 220 | redis_client.set("default_openvpn_topology_uuid", str(topology.id), ex=None) 221 | redis_client.set("default_openvpn_topology_key", str(topology.key), ex=None) 222 | return topology 223 | 224 | 225 | if __name__ == "__main__": 226 | from django.contrib.auth import get_user_model 227 | from swapper import load_model 228 | 229 | Ca = load_model("pki", "Ca") 230 | Cert = load_model("pki", "Cert") 231 | Template = load_model("config", "Template") 232 | Vpn = load_model("config", "Vpn") 233 | Credentials = load_model("connection", "Credentials") 234 | User = get_user_model() 235 | # We don't write with Django's cache mechanism because 236 | # it serializes the data and augment's it with Django specific 237 | # metadata. This creates unnecessary overhead when we are 238 | # reading data using redis-cli. 239 | redis_client = redis.Redis.from_url(settings.CACHES["default"]["LOCATION"]) 240 | 241 | create_admin() 242 | # Steps for creating new vpn client template with all the 243 | # required objects (CA, Certificate, VPN Server). 244 | default_ca = create_default_ca() 245 | default_cert = create_default_cert(default_ca) 246 | default_vpn = create_default_vpn( 247 | default_ca, 248 | default_cert, 249 | ) 250 | create_default_vpn_template(default_vpn) 251 | 252 | create_default_credentials() 253 | create_ssh_key_template() 254 | 255 | if env_bool(os.environ.get("USE_OPENWISP_TOPOLOGY")): 256 | Topology = load_model("topology", "Topology") 257 | create_default_topology(default_vpn) 258 | 259 | try: 260 | # Force RDB save to avoid data loss 261 | redis_client.save() 262 | except redis.exceptions.ResponseError: 263 | # Redis server may not support RDB save command, 264 | # so we ignore the error. 265 | pass 266 | -------------------------------------------------------------------------------- /images/openwisp_dashboard/module_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from openwisp.settings import MIDDLEWARE 4 | from openwisp.utils import request_scheme 5 | 6 | INSTALLED_APPS = [ 7 | "django.contrib.auth", 8 | "django.contrib.contenttypes", 9 | "django.contrib.sessions", 10 | "django.contrib.messages", 11 | "django.contrib.staticfiles", 12 | "django.contrib.humanize", 13 | "django.contrib.gis", 14 | # all-auth 15 | "django.contrib.sites", 16 | "openwisp_users.accounts", 17 | "allauth", 18 | "allauth.account", 19 | "allauth.socialaccount", 20 | "django_extensions", 21 | # openwisp modules 22 | "openwisp_users", 23 | # openwisp-controller 24 | "openwisp_controller.pki", 25 | "openwisp_controller.config", 26 | "openwisp_controller.geo", 27 | "openwisp_controller.connection", 28 | "openwisp_controller.subnet_division", 29 | # openwisp-monitoring 30 | "openwisp_monitoring.monitoring", 31 | "openwisp_monitoring.device", 32 | "openwisp_monitoring.check", 33 | "nested_admin", 34 | # openwisp-notification 35 | "openwisp_notifications", 36 | # openwisp-ipam 37 | "openwisp_ipam", 38 | # openwisp-network-topology 39 | "openwisp_network_topology", 40 | # openwisp-firmware-upgrader 41 | "openwisp_firmware_upgrader", 42 | # openwisp-radius 43 | "dj_rest_auth", 44 | "dj_rest_auth.registration", 45 | "openwisp_radius", 46 | # admin 47 | "openwisp_utils.admin_theme", 48 | "django.contrib.admin", 49 | "django.forms", 50 | # other dependencies 51 | "sortedm2m", 52 | "reversion", 53 | "leaflet", 54 | # rest framework 55 | "rest_framework", 56 | "rest_framework_gis", 57 | "rest_framework.authtoken", 58 | "django_filters", 59 | # social login 60 | "allauth.socialaccount.providers.facebook", 61 | "allauth.socialaccount.providers.google", 62 | # other dependencies 63 | "flat_json_widget", 64 | "private_storage", 65 | "drf_yasg", 66 | "import_export", 67 | "admin_auto_filters", 68 | "channels", 69 | "pipeline", 70 | "corsheaders", 71 | ] 72 | 73 | EXTENDED_APPS = [ 74 | "django_x509", 75 | "django_loci", 76 | ] 77 | MIDDLEWARE += [ 78 | "pipeline.middleware.MinifyHTMLMiddleware", 79 | ] 80 | # HTML minification with django pipeline 81 | PIPELINE = {"PIPELINE_ENABLED": True} 82 | # static files minification and invalidation with django-compress-staticfiles 83 | STORAGES = { 84 | "staticfiles": { 85 | "BACKEND": "openwisp_utils.storage.CompressStaticFilesStorage", 86 | }, 87 | } 88 | BROTLI_STATIC_COMPRESSION = False 89 | # pregenerate static gzip files to save CPU 90 | GZIP_STATIC_COMPRESSION = True 91 | 92 | API_BASEURL = f'{request_scheme()}://{os.environ["API_DOMAIN"]}' 93 | 94 | OPENWISP_NETWORK_TOPOLOGY_API_URLCONF = "openwisp_network_topology.urls" 95 | OPENWISP_MONITORING_API_URLCONF = "openwisp_monitoring.urls" 96 | OPENWISP_RADIUS_API_URLCONF = "openwisp_radius.urls" 97 | OPENWISP_NETWORK_TOPOLOGY_API_BASEURL = API_BASEURL 98 | OPENWISP_NOTIFICATIONS_HOST = API_BASEURL 99 | OPENWISP_CONTROLLER_API_HOST = API_BASEURL 100 | OPENWISP_MONITORING_API_BASEURL = API_BASEURL 101 | OPENWISP_FIRMWARE_API_BASEURL = API_BASEURL 102 | OPENWISP_RADIUS_API_BASEURL = API_BASEURL 103 | -------------------------------------------------------------------------------- /images/openwisp_dashboard/openvpn.json: -------------------------------------------------------------------------------- 1 | { 2 | "openvpn": [ 3 | { 4 | "server": "10.8.0.0 255.255.255.0", 5 | "name": "default", 6 | "mode": "server", 7 | "proto": "udp", 8 | "port": 1194, 9 | "dev_type": "tun", 10 | "dev": "tun0", 11 | "local": "", 12 | "comp_lzo": "no", 13 | "auth": "SHA1", 14 | "cipher": "none", 15 | "engine": "", 16 | "ca": "ca.pem", 17 | "cert": "cert.pem", 18 | "key": "key.pem", 19 | "pkcs12": "", 20 | "ns_cert_type": "", 21 | "mtu_disc": "no", 22 | "mtu_test": false, 23 | "fragment": 0, 24 | "mssfix": 1450, 25 | "keepalive": "10 120", 26 | "persist_tun": true, 27 | "persist_key": true, 28 | "tun_ipv6": false, 29 | "up": "", 30 | "up_delay": 0, 31 | "down": "", 32 | "script_security": 1, 33 | "user": "nobody", 34 | "group": "nogroup", 35 | "mute": 0, 36 | "status": "/var/log/tun0.status", 37 | "status_version": 1, 38 | "mute_replay_warnings": false, 39 | "secret": "", 40 | "reneg_sec": 0, 41 | "tls_timeout": 2, 42 | "tls_cipher": "", 43 | "remote_cert_tls": "", 44 | "float": false, 45 | "fast_io": true, 46 | "log": "", 47 | "verb": 3, 48 | "topology": "p2p", 49 | "tls_server": true, 50 | "dh": "dh.pem", 51 | "crl_verify": "revoked.crl", 52 | "duplicate_cn": false, 53 | "client_to_client": false, 54 | "client_cert_not_required": false, 55 | "username_as_common_name": false, 56 | "auth_user_pass_verify": "", 57 | "tls_auth": "" 58 | } 59 | ], 60 | "files": [ 61 | { 62 | "path": "ca.pem", 63 | "mode": "0644", 64 | "contents": "{{ ca }}" 65 | }, 66 | { 67 | "path": "cert.pem", 68 | "mode": "0644", 69 | "contents": "{{ cert }}" 70 | }, 71 | { 72 | "path": "key.pem", 73 | "mode": "0644", 74 | "contents": "{{ key }}" 75 | }, 76 | { 77 | "path": "dh.pem", 78 | "mode": "0644", 79 | "contents": "{{ dh }}" 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /images/openwisp_dashboard/urls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.conf.urls.static import static 5 | from django.contrib import admin 6 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 7 | from django.urls import include, path, reverse_lazy 8 | from django.views.generic import RedirectView 9 | from openwisp.utils import env_bool, openwisp_controller_urls 10 | 11 | index_redirect_view = RedirectView.as_view(url=reverse_lazy("admin:index")) 12 | 13 | urlpatterns = [ 14 | path("", index_redirect_view, name="index"), 15 | path("admin/", admin.site.urls), 16 | path("accounts/", include("openwisp_users.accounts.urls")), 17 | ] 18 | 19 | urlpatterns += openwisp_controller_urls() 20 | 21 | if env_bool(os.environ["USE_OPENWISP_MONITORING"]): 22 | urlpatterns += [ 23 | path("", include("openwisp_monitoring.urls")), 24 | ] 25 | 26 | if env_bool(os.environ["USE_OPENWISP_TOPOLOGY"]): 27 | from openwisp_network_topology.visualizer import urls as visualizer_urls 28 | 29 | urlpatterns += [path("topology/", include(visualizer_urls))] 30 | 31 | if env_bool(os.environ["USE_OPENWISP_FIRMWARE"]): 32 | # When using S3_REVERSE_PROXY feature of django-private-storage, 33 | # the storage backend reverse the "serve_private_file" URL 34 | # pattern in order to proxy the file with the correct URL. 35 | from openwisp_firmware_upgrader.private_storage.urls import ( 36 | urlpatterns as fw_private_storage_urls, 37 | ) 38 | 39 | urlpatterns += [ 40 | path( 41 | "", 42 | include((fw_private_storage_urls, "firmware"), namespace="firmware"), 43 | ) 44 | ] 45 | 46 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 47 | urlpatterns += staticfiles_urlpatterns() 48 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM freeradius/freeradius-server:3.2.7-alpine 2 | 3 | # hadolint ignore=DL3018 4 | RUN apk add --no-cache --update tzdata~=2024b-r1 \ 5 | postgresql17-client~=17.5-r0 && \ 6 | rm -rf /var/cache/apk/* /tmp/* 7 | 8 | RUN addgroup -S freerad && \ 9 | adduser -S freerad -G freerad 10 | 11 | CMD ["sh", "init_command.sh"] 12 | EXPOSE 1812/udp 1813/udp 13 | 14 | # hadolint ignore=DL3045 15 | COPY ./common/init_command.sh \ 16 | ./common/utils.sh ./ 17 | COPY ./openwisp_freeradius/raddb/ /etc/raddb/ 18 | RUN chown -R freerad:root /opt/etc/raddb/ && \ 19 | chown -R freerad:root /opt/var/log/ 20 | 21 | ENV TZ=UTC \ 22 | MODULE_NAME=freeradius \ 23 | DB_NAME=openwisp_db \ 24 | DB_USER=admin \ 25 | DB_PASS=admin \ 26 | DB_HOST=postgres \ 27 | DB_PORT=5432 \ 28 | DB_SSLMODE=disable \ 29 | DB_SSLKEY=None \ 30 | DB_SSLCERT=None \ 31 | DB_SSLROOTCERT=None \ 32 | DB_OPTIONS={} \ 33 | DASHBOARD_INTERNAL=dashboard.internal \ 34 | API_INTERNAL=api.internal \ 35 | DEBUG_MODE=False 36 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/raddb/dictionary: -------------------------------------------------------------------------------- 1 | ATTRIBUTE Expire-After 86400 integer 2 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/raddb/mods-enabled/rest: -------------------------------------------------------------------------------- 1 | rest { 2 | tls = {} 3 | connect_uri = "http://$ENV{API_INTERNAL}/api/v1/freeradius" 4 | authorize { 5 | uri = "${..connect_uri}/authorize/" 6 | method = 'post' 7 | body = 'json' 8 | data = '{"username": "%{User-Name}", "password": "%{User-Password}"}' 9 | tls = ${..tls} 10 | } 11 | 12 | # this section can be left empty 13 | authenticate {} 14 | 15 | post-auth { 16 | uri = "${..connect_uri}/postauth/" 17 | method = 'post' 18 | body = 'json' 19 | data = '{"username": "%{User-Name}", "password": "%{User-Password}", "reply": "%{reply:Packet-Type}", "called_station_id": "%{Called-Station-ID}", "calling_station_id": "%{Calling-Station-ID}"}' 20 | tls = ${..tls} 21 | } 22 | 23 | accounting { 24 | uri = "${..connect_uri}/accounting/" 25 | method = 'post' 26 | body = 'json' 27 | data = '{"status_type": "%{Acct-Status-Type}", "session_id": "%{Acct-Session-Id}", "unique_id": "%{Acct-Unique-Session-Id}", "username": "%{User-Name}", "realm": "%{Realm}", "nas_ip_address": "%{NAS-IP-Address}", "nas_port_id": "%{NAS-Port}", "nas_port_type": "%{NAS-Port-Type}", "session_time": "%{Acct-Session-Time}", "authentication": "%{Acct-Authentic}", "input_octets": "%{Acct-Input-Octets}", "output_octets": "%{Acct-Output-Octets}", "called_station_id": "%{Called-Station-Id}", "calling_station_id": "%{Calling-Station-Id}", "terminate_cause": "%{Acct-Terminate-Cause}", "service_type": "%{Service-Type}", "framed_protocol": "%{Framed-Protocol}", "framed_ip_address": "%{Framed-IP-Address}"}' 28 | tls = ${..tls} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/raddb/mods-enabled/sql: -------------------------------------------------------------------------------- 1 | # /etc/freeradius/mods-available/sql 2 | 3 | sql { 4 | driver = "rlm_sql_postgresql" 5 | dialect = "postgresql" 6 | radius_db = "host=$ENV{DB_HOST} port=$ENV{DB_PORT} dbname=$ENV{DB_NAME} user=$ENV{DB_USER} password=$ENV{DB_PASS} sslmode=$ENV{DB_SSLMODE} sslcert=$ENV{DB_SSLCERT} sslkey=$ENV{DB_SSLKEY} sslrootcert=$ENV{DB_SSLROOTCERT}" 7 | acct_table1 = "radacct" 8 | acct_table2 = "radacct" 9 | postauth_table = "radpostauth" 10 | authcheck_table = "radcheck" 11 | groupcheck_table = "radgroupcheck" 12 | authreply_table = "radreply" 13 | groupreply_table = "radgroupreply" 14 | usergroup_table = "radusergroup" 15 | delete_stale_sessions = yes 16 | client_table = "nas" 17 | read_clients = yes 18 | group_attribute = "SQL-Group" 19 | $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf 20 | pool { 21 | start = ${thread[pool].start_servers} 22 | min = ${thread[pool].min_spare_servers} 23 | max = ${thread[pool].max_servers} 24 | spare = ${thread[pool].max_spare_servers} 25 | uses = 0 26 | retry_delay = 30 27 | lifetime = 0 28 | idle_timeout = 60 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/raddb/mods-enabled/sqlcounter: -------------------------------------------------------------------------------- 1 | # The dailycounter is included by default in the freeradius conf 2 | 3 | sqlcounter dailycounter { 4 | sql_module_instance = sql 5 | dialect = postgresql 6 | 7 | counter_name = Daily-Session-Time 8 | check_name = Max-Daily-Session 9 | reply_name = Session-Timeout 10 | key = User-Name 11 | reset = daily 12 | 13 | query = "SELECT SUM(AcctSessionTime - GREATEST((%%b - EXTRACT(epoch FROM AcctStartTime)), 0)) \ 14 | FROM radacct \ 15 | WHERE UserName='%{${key}}' \ 16 | AND EXTRACT(epoch FROM AcctStartTime) + AcctSessionTime > '%%b'" 17 | } 18 | 19 | # The noresetcounter is included by default in the freeradius conf 20 | sqlcounter noresetcounter { 21 | sql_module_instance = sql 22 | dialect = postgresql 23 | 24 | counter_name = Max-All-Session-Time 25 | check_name = Max-All-Session 26 | key = User-Name 27 | reset = never 28 | 29 | $INCLUDE ${modconfdir}/sql/counter/${dialect}/${.:instance}.conf 30 | } 31 | 32 | # The dailybandwidthcounter is added for openwisp-radius 33 | sqlcounter dailybandwidthcounter { 34 | counter_name = Max-Daily-Session-Traffic 35 | check_name = Max-Daily-Session-Traffic 36 | sql_module_instance = sql 37 | key = 'User-Name' 38 | reset = daily 39 | query = "SELECT SUM(AcctOutputOctets) + SUM(AcctInputOctets) \ 40 | FROM radacct \ 41 | WHERE UserName = '%{${key}}' AND \ 42 | EXTRACT(epoch FROM AcctStartTime) + AcctSessionTime > '%%b'" 43 | } 44 | 45 | sqlcounter monthlycounter { 46 | sql_module_instance = sql 47 | dialect = postgresql 48 | 49 | counter_name = Monthly-Session-Time 50 | check_name = Max-Monthly-Session 51 | reply_name = Session-Timeout 52 | key = User-Name 53 | reset = monthly 54 | query = "SELECT SUM(AcctSessionTime - GREATEST((%%b - EXTRACT(epoch FROM AcctStartTime)), 0)) \ 55 | FROM radacct \ 56 | WHERE UserName='%{${key}}' \ 57 | AND EXTRACT(epoch FROM AcctStartTime) + AcctSessionTime > '%%b'" 58 | } 59 | 60 | sqlcounter expire_on_login { 61 | sql_module_instance = sql 62 | dialect = postgresql 63 | 64 | counter_name = Expire-After-Initial-Login 65 | check_name = Expire-After 66 | key = User-Name 67 | reset = never 68 | 69 | $INCLUDE ${modconfdir}/sql/counter/${dialect}/${.:instance}.conf 70 | } 71 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/raddb/radiusd.conf: -------------------------------------------------------------------------------- 1 | prefix = /usr 2 | exec_prefix = ${prefix} 3 | sysconfdir = /etc 4 | localstatedir = /var 5 | sbindir = ${exec_prefix}/sbin 6 | logdir = /opt/var/log/radius 7 | raddbdir = ${sysconfdir}/raddb 8 | radacctdir = /opt/var/log/radius/radacct 9 | name = radiusd 10 | confdir = ${raddbdir} 11 | modconfdir = ${confdir}/mods-config 12 | certdir = ${confdir}/certs 13 | cadir = ${confdir}/certs 14 | run_dir = ${localstatedir}/run/${name} 15 | db_dir = ${raddbdir} 16 | libdir = /usr/lib/freeradius 17 | pidfile = ${run_dir}/${name}.pid 18 | correct_escapes = true 19 | max_request_time = 30 20 | cleanup_delay = 5 21 | max_requests = 16384 22 | hostname_lookups = no 23 | 24 | log { 25 | destination = stdout 26 | auth = yes 27 | auth_badpass = yes 28 | auth_goodpass = yes 29 | } 30 | 31 | checkrad = ${sbindir}/checkrad 32 | security { 33 | user = freerad 34 | group = freerad 35 | allow_core_dumps = no 36 | max_attributes = 200 37 | reject_delay = 1 38 | status_server = yes 39 | allow_vulnerable_openssl = no 40 | } 41 | 42 | proxy_requests = yes 43 | $INCLUDE proxy.conf 44 | $INCLUDE clients.conf 45 | thread pool { 46 | start_servers = 5 47 | max_servers = 32 48 | min_spare_servers = 3 49 | max_spare_servers = 10 50 | max_requests_per_server = 0 51 | auto_limit_acct = no 52 | } 53 | 54 | modules { 55 | $INCLUDE mods-enabled/ 56 | } 57 | 58 | instantiate {} 59 | 60 | policy { 61 | $INCLUDE policy.d/ 62 | } 63 | $INCLUDE sites-enabled/ 64 | -------------------------------------------------------------------------------- /images/openwisp_freeradius/raddb/sites-enabled/default: -------------------------------------------------------------------------------- 1 | server default { 2 | listen { 3 | type = auth 4 | ipaddr = * 5 | port = 0 6 | limit { 7 | max_connections = 16 8 | lifetime = 0 9 | idle_timeout = 30 10 | } 11 | } 12 | 13 | listen { 14 | ipaddr = * 15 | port = 0 16 | type = acct 17 | limit {} 18 | } 19 | 20 | authorize { 21 | rest 22 | sql 23 | dailycounter 24 | noresetcounter 25 | dailybandwidthcounter 26 | } 27 | 28 | authenticate {} 29 | 30 | preacct { 31 | preprocess 32 | acct_unique 33 | suffix 34 | files 35 | } 36 | 37 | accounting { 38 | rest 39 | } 40 | 41 | session {} 42 | 43 | post-auth { 44 | rest 45 | 46 | Post-Auth-Type REJECT { 47 | rest 48 | } 49 | } 50 | 51 | pre-proxy {} 52 | post-proxy {} 53 | } 54 | -------------------------------------------------------------------------------- /images/openwisp_nfs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22 2 | 3 | # hadolint ignore=DL3018 4 | RUN apk add --no-cache --update --verbose \ 5 | tzdata~=2025b-r0 \ 6 | nfs-utils~=2.6.4-r4 && \ 7 | rm -rf /var/cache/apk/* /tmp/* 8 | 9 | COPY ./openwisp_nfs/init_command.sh /init_command.sh 10 | 11 | EXPOSE 111 111/udp 2049 2049/udp \ 12 | 32765 32765/udp 32766 32766/udp 32767 32767/udp 32768 32768/udp 13 | 14 | ENV TZ=UTC \ 15 | EXPORT_DIR="/exports" \ 16 | EXPORT_OPTS="10.0.0.0/8(rw,fsid=0,insecure,no_root_squash,no_subtree_check,sync)" 17 | 18 | CMD ["sh", "init_command.sh"] 19 | -------------------------------------------------------------------------------- /images/openwisp_nfs/init_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This file will NFSv3, I have not moved to 3 | # NFSv4 because terraform doesn't support the option 4 | # to use NFSv4 on Google Cloud, when terraform 5 | # is updated, we can move to NFSv4. 6 | 7 | set -ex 8 | 9 | mkdir -p $EXPORT_DIR/certs $EXPORT_DIR/postgres $EXPORT_DIR/static $EXPORT_DIR/media $EXPORT_DIR/html 10 | echo "$EXPORT_DIR $EXPORT_OPTS" >/etc/exports 11 | 12 | mount -t nfsd nfsd /proc/fs/nfsd 13 | # Fixed nlockmgr port 14 | echo 'fs.nfs.nlm_tcpport=32768' >>/etc/sysctl.conf 15 | echo 'fs.nfs.nlm_udpport=32768' >>/etc/sysctl.conf 16 | sysctl -p >/dev/null 17 | 18 | rpcbind -w 19 | rpc.nfsd -N 2 -V 3 -N 4 -N 4.1 8 20 | exportfs -arfv 21 | rpc.statd -p 32765 -o 32766 22 | rpc.mountd -N 2 -V 3 -N 4 -N 4.1 -p 32767 -F 23 | -------------------------------------------------------------------------------- /images/openwisp_nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.27.5-alpine 2 | 3 | RUN apk add --update --no-cache \ 4 | openssl~=3.3.3-r0 \ 5 | py3-pip~=24.3.1-r0 \ 6 | certbot~=3.0.1-r0 \ 7 | certbot-nginx~=3.0.1-r0 && \ 8 | rm -rf /var/cache/apk/* /tmp/* 9 | 10 | WORKDIR /etc/nginx/ 11 | CMD ["sh", "init_command.sh"] 12 | EXPOSE 80 443 13 | 14 | # DL3018 15 | COPY ./common/services.py \ 16 | ./common/init_command.sh \ 17 | ./common/utils.sh \ 18 | ./openwisp_nginx/ \ 19 | /etc/nginx/ 20 | 21 | RUN pip install --break-system-packages --no-cache-dir -r requirements.txt 22 | 23 | ENV MODULE_NAME=nginx \ 24 | PYTHONUNBUFFERED=1 \ 25 | DOLLAR=$ \ 26 | TZ=UTC \ 27 | SSL_CERT_MODE=Yes \ 28 | CERT_ADMIN_EMAIL=example@example.com \ 29 | NGINX_HTTP2=http2 \ 30 | NGINX_CLIENT_BODY_SIZE=30 \ 31 | NGINX_ADMIN_ALLOW_NETWORK=all \ 32 | NGINX_HTTPS_ALLOWED_IPS=all \ 33 | NGINX_HTTP_ALLOW=False \ 34 | NGINX_SERVER_NAME_HASH_BUCKET=32 \ 35 | NGINX_SSL_CONFIG='' \ 36 | NGINX_IP6_STRING='' \ 37 | NGINX_IP6_80_STRING='' \ 38 | NGINX_80_CONFIG='' \ 39 | NGINX_GZIP_SWITCH=on \ 40 | NGINX_GZIP_LEVEL=6 \ 41 | NGINX_GZIP_PROXIED=any \ 42 | NGINX_GZIP_MIN_LENGTH=1000 \ 43 | NGINX_GZIP_TYPES='text/plain image/svg+xml application/json application/javascript text/xml text/css application/xml application/x-font-ttf font/opentype' \ 44 | NGINX_CUSTOM_FILE=False \ 45 | NINGX_REAL_REMOTE_ADDR='$real_ip' \ 46 | # USWGI pass_port 47 | DASHBOARD_APP_PORT=8000 \ 48 | API_APP_PORT=8001 \ 49 | WEBSOCKET_APP_PORT=8002 \ 50 | # Application Service Name 51 | DASHBOARD_APP_SERVICE=dashboard \ 52 | API_APP_SERVICE=api \ 53 | WEBSOCKET_APP_SERVICE=websocket \ 54 | # Listen domains 55 | DASHBOARD_DOMAIN=dashboard.example.com \ 56 | API_DOMAIN=api.example.com \ 57 | # Inter container communication domains 58 | DASHBOARD_INTERNAL=dashboard.internal \ 59 | API_INTERNAL=api.internal 60 | -------------------------------------------------------------------------------- /images/openwisp_nginx/get_domain.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import tldextract 5 | 6 | ext = tldextract.extract(os.environ["API_DOMAIN"]) 7 | sys.stdout.write(ext.registered_domain) 8 | -------------------------------------------------------------------------------- /images/openwisp_nginx/nginx.template.conf: -------------------------------------------------------------------------------- 1 | # Nginx configuration template, change any 2 | # configuration here to manipulate nginx server. 3 | # Changes in given http block reflect in 4 | # all openwisp server blocks. 5 | 6 | user nginx; 7 | worker_processes 1; 8 | 9 | error_log /var/log/nginx/error.log warn; 10 | pid /var/run/nginx.pid; 11 | 12 | 13 | events { 14 | worker_connections 1024; 15 | } 16 | 17 | http { 18 | include /etc/nginx/mime.types; 19 | default_type application/octet-stream; 20 | 21 | log_format main '[${HOSTNAME}] - ${DOLLAR}remote_user [${DOLLAR}time_local] "${DOLLAR}request" ' 22 | 'status: ${DOLLAR}status ${DOLLAR}body_bytes_sent "${DOLLAR}http_referer" ' 23 | '"${DOLLAR}http_user_agent" http_x_forwarded_for: ' 24 | '${DOLLAR}http_x_forwarded_for - remote_addr: ${DOLLAR}remote_addr - ' 25 | 'realip_remote_addr: ${DOLLAR}realip_remote_addr - real_ip: ${DOLLAR}real_ip'; 26 | 27 | # Nginx Logging 28 | access_log /dev/stdout main; 29 | error_log /dev/stdout error; 30 | 31 | sendfile on; 32 | keepalive_timeout 65; 33 | 34 | # Map $real_ip 35 | map ${DOLLAR}http_x_forwarded_for ${DOLLAR}real_ip { 36 | ~^(\d+\.\d+\.\d+\.\d+) ${DOLLAR}1; 37 | default ${DOLLAR}remote_addr; 38 | } 39 | 40 | server_names_hash_bucket_size $NGINX_SERVER_NAME_HASH_BUCKET; 41 | server_tokens off; 42 | 43 | server { 44 | listen 80 default_server; 45 | location /status { 46 | access_log off; 47 | return 200 "Healthy\n"; 48 | } 49 | } 50 | 51 | include /etc/nginx/conf.d/*.conf; 52 | } 53 | -------------------------------------------------------------------------------- /images/openwisp_nginx/openwisp.internal.template.conf: -------------------------------------------------------------------------------- 1 | # Internal Communication: inter-container communication 2 | 3 | server { 4 | listen 80; 5 | server_name $DOMAIN; 6 | 7 | # Nginx Logging 8 | access_log /dev/stdout main; 9 | error_log /dev/stdout error; 10 | 11 | location / { 12 | try_files ${DOLLAR}uri @uwsgi; 13 | } 14 | 15 | location @uwsgi { 16 | include uwsgi_params; 17 | uwsgi_pass ${APP_SERVICE}:${APP_PORT}; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /images/openwisp_nginx/openwisp.ssl.80.template.conf: -------------------------------------------------------------------------------- 1 | # Redirect all HTTP traffic to HTTPS 2 | 3 | server { 4 | listen 80; 5 | $NGINX_IP6_80_STRING 6 | server_name $DASHBOARD_DOMAIN $API_DOMAIN; 7 | 8 | # Necessary for Let's Encrypt domain name ownership validation 9 | location /.well-known/ { 10 | try_files ${DOLLAR}uri /dev/null =404; 11 | } 12 | return 301 https://${DOLLAR}host${DOLLAR}request_uri; 13 | } 14 | -------------------------------------------------------------------------------- /images/openwisp_nginx/openwisp.ssl.template.conf: -------------------------------------------------------------------------------- 1 | # Nginx server - Openwisp SSL 2 | 3 | server { 4 | listen 443 ssl $NGINX_HTTP2; 5 | $NGINX_IP6_STRING 6 | server_name $DOMAIN; 7 | root /opt/openwisp/public/; 8 | index index.html index.htm; 9 | 10 | client_max_body_size ${NGINX_CLIENT_BODY_SIZE}M; 11 | 12 | # SSL configurations 13 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 14 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 15 | ssl_session_cache shared:SSL:20m; 16 | ssl_session_timeout 10m; 17 | ssl_protocols TLSv1.2 TLSv1.3; 18 | ssl_prefer_server_ciphers on; 19 | # generated 2022-02-02, Mozilla Guideline v5.6, nginx 1.17.7, OpenSSL 1.1.1k, intermediate configuration 20 | # https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1k&guideline=5.6 21 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 22 | 23 | # Additional Security Headers 24 | add_header X-XSS-Protection "1; mode=block" always; 25 | add_header X-Content-Type-Options "nosniff" always; 26 | add_header Referrer-Policy "same-site" always; 27 | add_header Permissions-Policy "interest-cohort=()" always; 28 | add_header Strict-Transport-Security "max-age=31536000" always; 29 | add_header Content-Security-Policy "default-src http: https: data: blob: 'unsafe-inline'; script-src 'unsafe-eval' https: 'unsafe-inline' 'self'; frame-ancestors 'self'; connect-src *.${ROOT_DOMAIN} wss: 'self'; worker-src https://${DOMAIN} blob: 'self';" always; 30 | 31 | # GZIP Configurations 32 | gzip ${NGINX_GZIP_SWITCH}; 33 | gzip_static ${NGINX_GZIP_SWITCH}; 34 | gzip_comp_level ${NGINX_GZIP_LEVEL}; 35 | gzip_proxied ${NGINX_GZIP_PROXIED}; 36 | gzip_min_length ${NGINX_GZIP_MIN_LENGTH}; 37 | gzip_types ${NGINX_GZIP_TYPES}; 38 | 39 | # Additional Settings 40 | $NGINX_SSL_CONFIG 41 | 42 | # Nginx Logging 43 | access_log /dev/stdout main; 44 | error_log /dev/stdout error; 45 | 46 | # Necessary for Let's Encrypt Domain Name ownership validation 47 | location /.well-known/ { 48 | try_files ${DOLLAR}uri /dev/null =404; 49 | } 50 | # Websocket 51 | location /ws/ { 52 | rewrite ^/(.*) /${DOLLAR}1 break; 53 | proxy_set_header X-Real-IP ${DOLLAR}remote_addr; 54 | proxy_set_header X-Forwarded-For ${DOLLAR}proxy_add_x_forwarded_for; 55 | proxy_set_header Host ${DOLLAR}http_host; 56 | proxy_redirect off; 57 | proxy_pass http://${WEBSOCKET_APP_SERVICE}:${WEBSOCKET_APP_PORT}; 58 | proxy_http_version 1.1; 59 | proxy_set_header Upgrade ${DOLLAR}http_upgrade; 60 | proxy_set_header Connection "upgrade"; 61 | } 62 | location /admin/ { 63 | try_files /custom/maintenance.html ${DOLLAR}uri @uwsgi; 64 | allow $NGINX_ADMIN_ALLOW_NETWORK; 65 | deny all; 66 | } 67 | location /static/ { 68 | try_files /custom${DOLLAR}uri /${DOLLAR}uri; 69 | } 70 | location / { 71 | try_files /custom/maintenance.html ${DOLLAR}uri ${DOLLAR}uri/index.html @uwsgi; 72 | } 73 | location /media/ { 74 | alias /opt/openwisp/public/media/; 75 | } 76 | location @uwsgi { 77 | uwsgi_pass ${APP_SERVICE}:${APP_PORT}; 78 | include uwsgi_params; 79 | uwsgi_param HTTP_X_FORWARDED_PROTO https; 80 | uwsgi_param REMOTE_ADDR ${NINGX_REAL_REMOTE_ADDR}; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /images/openwisp_nginx/openwisp.template.conf: -------------------------------------------------------------------------------- 1 | # Nginx server - Openwisp without SSL 2 | 3 | server { 4 | listen 80; 5 | $NGINX_IP6_80_STRING 6 | server_name $DOMAIN; 7 | root /opt/openwisp/public/; 8 | # Nginx Logging 9 | access_log /dev/stdout main; 10 | error_log /dev/stdout error; 11 | 12 | # GZIP Configurations 13 | gzip ${NGINX_GZIP_SWITCH}; 14 | gzip_comp_level ${NGINX_GZIP_LEVEL}; 15 | gzip_proxied ${NGINX_GZIP_PROXIED}; 16 | gzip_min_length ${NGINX_GZIP_MIN_LENGTH}; 17 | gzip_types ${NGINX_GZIP_TYPES}; 18 | 19 | # Additional Settings 20 | $NGINX_80_CONFIG 21 | 22 | add_header Strict-Transport-Security "max-age=31536000"; 23 | add_header X-Content-Type-Options nosniff; 24 | 25 | location /ws/ { 26 | rewrite ^/(.*) /${DOLLAR}1 break; 27 | proxy_set_header X-Real-IP ${DOLLAR}remote_addr; 28 | proxy_set_header X-Forwarded-For ${DOLLAR}proxy_add_x_forwarded_for; 29 | proxy_set_header Host ${DOLLAR}http_host; 30 | proxy_redirect off; 31 | proxy_pass http://${WEBSOCKET_APP_SERVICE}:${WEBSOCKET_APP_PORT}; 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade ${DOLLAR}http_upgrade; 34 | proxy_set_header Connection "upgrade"; 35 | } 36 | location /static/ { 37 | try_files /custom${DOLLAR}uri /${DOLLAR}uri; 38 | } 39 | location / { 40 | error_page 403 = @deny; 41 | allow $NGINX_HTTPS_ALLOWED_IPS; 42 | deny all; 43 | try_files /custom/maintenance.html ${DOLLAR}uri ${DOLLAR}uri/index.html @uwsgi; 44 | } 45 | location /media/ { 46 | alias /opt/openwisp/public/media/; 47 | } 48 | location @uwsgi { 49 | uwsgi_pass ${APP_SERVICE}:${APP_PORT}; 50 | include uwsgi_params; 51 | uwsgi_param HTTP_X_FORWARDED_PROTO http; 52 | uwsgi_param REMOTE_ADDR ${NINGX_REAL_REMOTE_ADDR}; 53 | } 54 | location @deny { 55 | return 301 https://${DOLLAR}host${DOLLAR}request_uri; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /images/openwisp_nginx/requirements.txt: -------------------------------------------------------------------------------- 1 | tldextract~=5.3.0 2 | -------------------------------------------------------------------------------- /images/openwisp_openvpn/Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint ignore=DL3007 2 | FROM kylemanna/openvpn:2.4 3 | 4 | RUN apk add --no-cache \ 5 | curl~=7.79.1-r1 \ 6 | tzdata~=2022a-r0 \ 7 | supervisor~=4.2.0-r0 && \ 8 | rm -rf /var/cache/apk/* /tmp/* 9 | CMD ["sh", "init_command.sh"] 10 | EXPOSE 1194 11 | 12 | ENV MODULE_NAME=openvpn \ 13 | DASHBOARD_APP_SERVICE=dashboard \ 14 | DASHBOARD_INTERNAL=dashboard.internal \ 15 | API_INTERNAL=api.internal \ 16 | DB_NAME=openwisp_db \ 17 | TZ=UTC \ 18 | DB_USER=admin \ 19 | DB_PASS=admin \ 20 | DB_HOST=postgres \ 21 | DB_PORT=5432 \ 22 | DB_SSLMODE=disable \ 23 | DB_SSLKEY=None \ 24 | DB_SSLCERT=None \ 25 | DB_SSLROOTCERT=None \ 26 | DB_OPTIONS={} \ 27 | TOPLOGY_UPDATE_INTERVAL=3 28 | 29 | # hadolint ignore=DL3045 30 | COPY ./common/init_command.sh \ 31 | ./common/utils.sh \ 32 | ./openwisp_openvpn/ ./ 33 | -------------------------------------------------------------------------------- /images/openwisp_openvpn/openvpn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script will be called by cronjob to 4 | # update OpenVPN configurations periodically. 5 | cd / 6 | source /utils.sh 7 | 8 | openvpn_config 9 | openvpn_config_checksum 10 | 11 | if [ "${OFILE}" != "${NFILE}" ]; then 12 | openvpn_config_download 13 | supervisorctl restart openvpn 14 | fi 15 | -------------------------------------------------------------------------------- /images/openwisp_openvpn/revokelist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script will be called by cronjob to 4 | # update CRL periodically. 5 | cd / 6 | source /utils.sh 7 | 8 | openvpn_config 9 | crl_download 10 | supervisorctl restart openvpn 11 | -------------------------------------------------------------------------------- /images/openwisp_openvpn/send-topology.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | COMMAND="cat /var/log/tun0.status" 4 | # Upload the topology data to OpenWISP 5 | $COMMAND | curl --silent -X POST \ 6 | --data-binary @- \ 7 | --header "Content-Type: text/plain" \ 8 | $API_INTERNAL/api/v1/network-topology/topology/$TOPOLOGY_UUID/receive/?key=$TOPOLOGY_KEY 9 | _ret=$? 10 | echo '' 11 | exit $_ret 12 | -------------------------------------------------------------------------------- /images/openwisp_openvpn/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/run/supervisord.sock 3 | 4 | [supervisorctl] 5 | serverurl=unix:///run/supervisord.sock 6 | 7 | [rpcinterface:supervisor] 8 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 9 | 10 | [supervisord] 11 | nodaemon=true 12 | user=root 13 | logfile=/dev/stdout 14 | logfile_maxbytes=0 15 | loglevel=info 16 | pidfile=/supervisord.pid 17 | 18 | [program:openvpn] 19 | user=root 20 | directory=/ 21 | command=/usr/sbin/openvpn --config %(ENV_VPN_NAME)s.conf 22 | autostart=true 23 | autorestart=true 24 | stopsignal=INT 25 | stdout_logfile=/dev/stdout 26 | stderr_logfile=/dev/stdout 27 | # Set logfile maxbytes to 0 to 28 | # avoid invalid seek error 29 | stdout_logfile_maxbytes=0 30 | stderr_logfile_maxbytes=0 31 | -------------------------------------------------------------------------------- /images/openwisp_postfix/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22 2 | 3 | WORKDIR /opt/openwisp/ 4 | RUN apk add --no-cache --upgrade \ 5 | openssl~=3.5.0-r0 \ 6 | cyrus-sasl~=2.1.28-r8 \ 7 | cyrus-sasl-login~=2.1.28-r8 && \ 8 | apk add --no-cache \ 9 | postfix~=3.10.2-r0 \ 10 | rsyslog~=8.2410.0-r1 \ 11 | tzdata~=2025b-r0 && \ 12 | rm -rf /tmp/* /var/cache/apk/* 13 | 14 | CMD ["sh", "init_command.sh"] 15 | EXPOSE 25 16 | 17 | COPY ./openwisp_postfix/rsyslog.conf /etc/rsyslog.conf 18 | COPY ./common/init_command.sh \ 19 | ./common/utils.sh \ 20 | /opt/openwisp/ 21 | 22 | ENV MODULE_NAME=postfix \ 23 | TZ=UTC \ 24 | POSTFIX_MYHOSTNAME=example.org \ 25 | POSTFIX_ALLOWED_SENDER_DOMAINS=example.org \ 26 | POSTFIX_RELAYHOST=null \ 27 | POSTFIX_DESTINATION='$mydomain, $myhostname' \ 28 | POSTFIX_RELAYHOST_USERNAME=null \ 29 | POSTFIX_RELAYHOST_PASSWORD=null \ 30 | POSTFIX_RELAYHOST_TLS_LEVEL=may \ 31 | POSTFIX_MYNETWORKS='127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128' \ 32 | POSTFIX_MESSAGE_SIZE_LIMIT=0 \ 33 | POSTFIX_DEBUG_MYNETWORKS=null 34 | -------------------------------------------------------------------------------- /images/openwisp_postfix/rsyslog.conf: -------------------------------------------------------------------------------- 1 | 2 | $ModLoad immark.so # provides --MARK-- message capability 3 | $ModLoad imuxsock.so # provides support for local system logging (e.g. via logger command) 4 | 5 | # default permissions for all log files. 6 | $FileOwner root 7 | $FileGroup adm 8 | $FileCreateMode 0640 9 | $DirCreateMode 0755 10 | $Umask 0022 11 | 12 | *.error /dev/stdout 13 | mail.* /dev/stdout 14 | -------------------------------------------------------------------------------- /images/openwisp_websocket/Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint ignore=DL3007 2 | FROM openwisp/openwisp-base:latest 3 | 4 | WORKDIR /opt/openwisp/ 5 | 6 | COPY --chown=openwisp:root ./common/ \ 7 | ./openwisp_websocket/daphne.conf \ 8 | /opt/openwisp/ 9 | COPY --chown=openwisp:root ./openwisp_websocket/module_settings.py \ 10 | ./openwisp_websocket/urls.py \ 11 | /opt/openwisp/openwisp/ 12 | 13 | ARG WEBSOCKET_APP_PORT=8002 14 | ENV MODULE_NAME=websocket \ 15 | DJANGO_WEBSOCKET_HOST=0.0.0.0 \ 16 | CONTAINER_PORT=$WEBSOCKET_APP_PORT 17 | 18 | EXPOSE $WEBSOCKET_APP_PORT 19 | CMD ["supervisord", "--nodaemon", "--configuration", "daphne.conf"] 20 | -------------------------------------------------------------------------------- /images/openwisp_websocket/daphne.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=openwisp 4 | logfile=/dev/stdout 5 | logfile_maxbytes=0 6 | loglevel=info 7 | 8 | [program:daphne] 9 | user=openwisp 10 | directory=/opt/openwisp/ 11 | command=/usr/local/bin/daphne -b %(ENV_DJANGO_WEBSOCKET_HOST)s -p %(ENV_CONTAINER_PORT)s --proxy-headers openwisp.asgi:application --access-log - 12 | autostart=true 13 | autorestart=true 14 | stopsignal=INT 15 | stdout_logfile=/dev/stdout 16 | stderr_logfile=/dev/stdout 17 | # Set logfile maxbytes to 0 to 18 | # avoid invalid seek error 19 | stdout_logfile_maxbytes=0 20 | stderr_logfile_maxbytes=0 21 | -------------------------------------------------------------------------------- /images/openwisp_websocket/module_settings.py: -------------------------------------------------------------------------------- 1 | INSTALLED_APPS = [ 2 | "django.contrib.auth", 3 | "django.contrib.contenttypes", 4 | "django.contrib.sessions", 5 | "django.contrib.messages", 6 | "django.contrib.staticfiles", 7 | "django.contrib.gis", 8 | # all-auth 9 | "django.contrib.sites", 10 | "allauth", 11 | "allauth.account", 12 | "allauth.socialaccount", 13 | # openwisp modules 14 | "openwisp_users", 15 | # openwisp-controller 16 | "openwisp_controller.pki", 17 | "openwisp_controller.config", 18 | "openwisp_controller.geo", 19 | "openwisp_controller.connection", 20 | "openwisp_controller.subnet_division", 21 | "flat_json_widget", 22 | "openwisp_notifications", 23 | # openwisp-ipam 24 | "openwisp_ipam", 25 | # openwisp-network-topology 26 | "openwisp_network_topology", 27 | "openwisp_utils.admin_theme", 28 | # admin 29 | "django.contrib.admin", 30 | "django.forms", 31 | # other dependencies 32 | "sortedm2m", 33 | "reversion", 34 | "leaflet", 35 | # rest framework 36 | "rest_framework", 37 | "rest_framework_gis", 38 | "django_filters", 39 | # other packages 40 | "private_storage", 41 | "channels", 42 | "drf_yasg", 43 | ] 44 | 45 | EXTENDED_APPS = [ 46 | "django_x509", 47 | "django_loci", 48 | ] 49 | -------------------------------------------------------------------------------- /images/openwisp_websocket/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /qa-format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # format shell scripts 5 | shfmt -w -l . 6 | 7 | # format python files 8 | openwisp-qa-format 9 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | docker~=7.1.0 2 | openwisp-utils[qa,selenium] @ https://github.com/openwisp/openwisp-utils/tarball/1.2 3 | -------------------------------------------------------------------------------- /run-qa-checks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # will be 1 by default when run through github actions 6 | CI=${CI:-false} 7 | 8 | echo '' 9 | echo 'Python file QA checks...' 10 | openwisp-qa-check --skip-checkmigrations 11 | 12 | if [ "$CI" = "false" ]; then 13 | echo 'Shell scripts QA checks ...' 14 | # check shell scripts formatting 15 | sh_files=$(shfmt -f .) 16 | shfmt -d . 17 | fi 18 | 19 | echo '' 20 | echo 'Dockerfile QA checks...' 21 | 22 | hadolint ./images/openwisp_freeradius/Dockerfile 23 | hadolint ./images/openwisp_nfs/Dockerfile 24 | hadolint ./images/openwisp_postfix/Dockerfile 25 | hadolint ./images/openwisp_base/Dockerfile 26 | hadolint ./images/openwisp_api/Dockerfile 27 | hadolint ./images/openwisp_dashboard/Dockerfile 28 | hadolint ./images/openwisp_nginx/Dockerfile 29 | hadolint ./images/openwisp_openvpn/Dockerfile 30 | hadolint ./images/openwisp_websocket/Dockerfile 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | line_length=88 3 | multi_line_output=3 4 | use_parentheses=True 5 | include_trailing_comma=True 6 | force_grid_wrap=0 7 | 8 | [flake8] 9 | exclude = build/common/openwisp/settings.py, build/openwisp_dashboard/module_settings.py 10 | max-line-length = 88 11 | -------------------------------------------------------------------------------- /tests/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "driver": "chromium", 3 | "headless": true, 4 | "app_url": "https://dashboard.openwisp.org", 5 | "api_url": "https://api.openwisp.org", 6 | "load_init_data": true, 7 | "logs": false, 8 | "logs_file": "/tmp/odocker.log", 9 | "username": "admin", 10 | "password": "admin", 11 | "services_max_retries": 25, 12 | "services_delay_retries": 5 13 | } 14 | -------------------------------------------------------------------------------- /tests/data.py: -------------------------------------------------------------------------------- 1 | # Initial data for running the tests 2 | 3 | from openwisp_controller.config.models import Config, Device 4 | from openwisp_radius.models import ( 5 | OrganizationRadiusSettings, 6 | RadiusGroup, 7 | RadiusUserGroup, 8 | ) 9 | from openwisp_users.models import Organization, OrganizationUser, User 10 | 11 | 12 | def get_organization(): 13 | """Fetch default organization.""" 14 | return Organization.objects.get(slug="default") 15 | 16 | 17 | def get_admin(): 18 | """Fetch superuser: admin.""" 19 | return User.objects.get(username="admin") 20 | 21 | 22 | def get_default_radius_group(): 23 | """Fetch "default-users" radius group.""" 24 | return RadiusGroup.objects.get(name="default-users") 25 | 26 | 27 | def set_default_radius_token(radiusOrg): 28 | """Set "defaultapitoken" to the given organization.""" 29 | radiusConf = OrganizationRadiusSettings.objects.filter(organization=radiusOrg) 30 | if not radiusConf.exists(): 31 | radiusConf = OrganizationRadiusSettings() 32 | radiusConf.organization = radiusOrg 33 | radiusConf.token = "defaultapitoken" 34 | radiusConf.save() 35 | return radiusConf 36 | 37 | 38 | def create_default_organizationUser(defOrg, admin): 39 | """Add superuser "admin" OrganizationUser of "default" organization.""" 40 | orgUser = OrganizationUser.objects.filter(organization=defOrg) 41 | if not orgUser.exists(): 42 | orgUser = OrganizationUser() 43 | orgUser.organization = defOrg 44 | orgUser.user = admin 45 | orgUser.full_clean() 46 | orgUser.save() 47 | return orgUser 48 | 49 | 50 | def create_default_radiusUser(admin, radGroup): 51 | """Add superuser "admin" to "default-users" radius user group.""" 52 | radiusUser = RadiusUserGroup.objects.filter(username="admin") 53 | if not radiusUser.exists(): 54 | radiusUser = RadiusUserGroup() 55 | radiusUser.group = radGroup 56 | radiusUser.user = admin 57 | radiusUser.full_clean() 58 | radiusUser.save() 59 | return radiusUser 60 | 61 | 62 | def create_device(organization): 63 | if Device.objects.filter(name="test-device").exists(): 64 | return Device.objects.get(name="test-device") 65 | device = Device( 66 | name="test-device", mac_address="11:22:33:44:55:66", organization=organization 67 | ) 68 | device.full_clean() 69 | device.save() 70 | config = Config(device=device, backend="netjsonconfig.OpenWrt") 71 | config.full_clean() 72 | config.save() 73 | return device 74 | 75 | 76 | def setup(): 77 | defOrg = get_organization() 78 | admin = get_admin() 79 | radGroup = get_default_radius_group() 80 | create_default_organizationUser(defOrg, admin) 81 | create_default_radiusUser(admin, radGroup) 82 | set_default_radius_token(defOrg) 83 | create_device(defOrg) 84 | 85 | 86 | if __name__ == "__main__": 87 | setup() 88 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import ssl 4 | import subprocess 5 | import time 6 | from time import sleep 7 | 8 | import docker 9 | from openwisp_utils.tests import SeleniumTestMixin 10 | from selenium.common.exceptions import NoAlertPresentException 11 | from selenium.webdriver.common.by import By 12 | 13 | 14 | class TestConfig: 15 | """Configuration class for setting up test parameters and utilities.""" 16 | 17 | def shortDescription(self): 18 | """Return a short description for the test.""" 19 | return None 20 | 21 | docker_client = docker.from_env() 22 | ctx = ssl.create_default_context() 23 | ctx.check_hostname = False 24 | ctx.verify_mode = ssl.CERT_NONE 25 | 26 | config_file = os.path.join(os.path.dirname(__file__), "config.json") 27 | root_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..") 28 | with open(config_file) as json_file: 29 | config = json.load(json_file) 30 | 31 | 32 | class TestUtilities(SeleniumTestMixin, TestConfig): 33 | """Utility functions for testing.""" 34 | 35 | objects_to_delete = [] 36 | browser = "chrome" 37 | 38 | def setUp(self): 39 | # Override TestSeleniumMixin setUp which uses 40 | # Django methods to create superuser 41 | return 42 | 43 | def login(self, username=None, password=None, driver=None): 44 | super().login(username, password, driver) 45 | # Workaround for JS logic in chart-utils.js 46 | # which fails to perform a XHR request 47 | # during automated tests, it seems that the 48 | # lack of pause causes the request to fail randomly 49 | sleep(0.5) 50 | 51 | def _ignore_location_alert(self, driver=None): 52 | """Accept alerts related to location not found. 53 | 54 | Parameters: 55 | 56 | - driver (selenium.webdriver, optional): The Selenium WebDriver 57 | instance. Defaults to `self.base_driver`. 58 | """ 59 | expected_msg = "Could not find any address related to this location." 60 | if not driver: 61 | driver = self.base_driver 62 | time.sleep(2) # Wait for the alert to appear 63 | try: 64 | window_alert = driver.switch_to.alert 65 | if expected_msg in window_alert.text: 66 | window_alert.accept() 67 | except NoAlertPresentException: 68 | pass # No alert is okay. 69 | 70 | def _click_save_btn(self, driver=None): 71 | """Click the save button in the admin interface. 72 | 73 | Parameters: 74 | 75 | - driver (selenium.webdriver, optional): The Selenium WebDriver 76 | instance. Defaults to `self.base_driver`. 77 | """ 78 | if not driver: 79 | driver = self.base_driver 80 | # Scroll to the top of the page. This will ensure that the save 81 | # button is visible and clickable. 82 | driver.execute_script("window.scrollTo(0, 0);") 83 | self.find_element(By.NAME, "_save", driver=driver).click() 84 | 85 | def create_superuser( 86 | self, 87 | email="test@user.com", 88 | username="test_superuser", 89 | password="randomPassword01!", 90 | driver=None, 91 | ): 92 | """Create a new superuser. 93 | 94 | Parameters: 95 | 96 | - email (str, optional): The email address of the superuser. 97 | Defaults to 'test@user.com'. 98 | - username (str, optional): The username of the superuser. 99 | Defaults to 'test_superuser'. 100 | - password (str, optional): The password for the superuser. 101 | Defaults to 'randomPassword01!'. 102 | - driver (selenium.webdriver, optional): The Selenium WebDriver 103 | instance. Defaults to `self.base_driver`. 104 | """ 105 | if not driver: 106 | driver = self.base_driver 107 | self.open("/admin/openwisp_users/user/add/", driver=driver) 108 | self.find_element(By.NAME, "username", driver=driver).send_keys(username) 109 | self.find_element(By.NAME, "email", driver=driver).send_keys(email) 110 | self.find_element(By.NAME, "password1", driver=driver).send_keys(password) 111 | self.find_element(By.NAME, "password2", driver=driver).send_keys(password) 112 | self.find_element(By.NAME, "is_superuser", driver=driver).click() 113 | self._click_save_btn(driver) 114 | self.objects_to_delete.append(driver.current_url) 115 | self._click_save_btn(driver) 116 | self._wait_until_page_ready() 117 | self.wait_for_visibility(By.ID, "content", driver=driver, timeout=10) 118 | 119 | def get_resource(self, resource_name, path, select_field="field-name", driver=None): 120 | """Navigate to a resource's change form page. 121 | 122 | Parameters: 123 | 124 | - resource_name (str): The name of the resource to find. 125 | - path (str): The path to the resource in the admin interface. 126 | - select_field (str, optional): The field used to identify the 127 | resource. Defaults to 'field-name'. 128 | - driver (selenium.webdriver, optional): The Selenium WebDriver 129 | instance. Defaults to `self.base_driver`. 130 | """ 131 | if not driver: 132 | driver = self.base_driver 133 | self.open(path, driver=driver) 134 | resources = self.find_elements( 135 | By.CLASS_NAME, select_field, wait_for="presence", driver=driver 136 | ) 137 | for resource in resources: 138 | if len(resource.find_elements(By.LINK_TEXT, resource_name)): 139 | resource.find_element(By.LINK_TEXT, resource_name).click() 140 | break 141 | self._wait_until_page_ready() 142 | 143 | def select_resource(self, name, driver=None): 144 | """Select a resource by name. 145 | 146 | Parameters: 147 | 148 | - name (str): The name of the resource to select. 149 | - driver (selenium.webdriver, optional): The Selenium WebDriver 150 | instance. Defaults to `self.base_driver`. 151 | """ 152 | if not driver: 153 | driver = self.base_driver 154 | path = f'//a[contains(text(), "{name}")]/../..//input[@name="_selected_action"]' 155 | self.find_element(By.XPATH, path, driver=driver).click() 156 | 157 | def action_on_resource(self, name, path, option, driver=None): 158 | """Perform an action on a resource. 159 | 160 | Parameters: 161 | 162 | - name (str): The name of the resource to select. 163 | - path (str): The path to the resource list page. 164 | - option (str): The value of the option to select. 165 | - driver (selenium.webdriver, optional): The Selenium WebDriver 166 | instance. Defaults to `self.base_driver`. 167 | """ 168 | if not driver: 169 | driver = self.base_driver 170 | self.open(path, driver=driver) 171 | self.select_resource(name) 172 | self.find_element(By.NAME, "action", driver=driver).find_element( 173 | By.XPATH, 174 | f'//option[@value="{option}"]', 175 | ).click() 176 | self.find_element(By.NAME, "index", driver=driver).click() 177 | 178 | def console_error_check(self, driver=None): 179 | """Check for JavaScript errors in the console. 180 | 181 | Parameters: 182 | 183 | - driver (selenium.webdriver, optional): The Selenium WebDriver 184 | instance. Defaults to `self.base_driver`. 185 | 186 | Returns: 187 | list: A list of JavaScript error messages. 188 | """ 189 | if not driver: 190 | driver = self.base_driver 191 | console_logs = [] 192 | logs = self.get_browser_logs(driver=driver) 193 | for logentry in logs: 194 | if logentry["level"] == "SEVERE": 195 | # Ignore error generated due to "leaflet" issue 196 | # https://github.com/makinacorpus/django-leaflet/pull/380 197 | if "leaflet" in logentry["message"]: 198 | continue 199 | # Ignore error generated due to "beforeunload" chrome issue 200 | # https://stackoverflow.com/questions/10680544/beforeunload-chrome-issue 201 | if "beforeunload" in logentry["message"]: 202 | continue 203 | console_logs.append(logentry["message"]) 204 | return console_logs 205 | 206 | def create_mobile_location(self, location_name, driver=None): 207 | """Create a new mobile location. 208 | 209 | Parameters: 210 | 211 | - location_name (str): The name of the new location. 212 | - driver (selenium.webdriver, optional): The Selenium WebDriver 213 | instance. Defaults to `self.base_driver`. 214 | """ 215 | if not driver: 216 | driver = self.base_driver 217 | self.open("/admin/geo/location/add/", driver=driver) 218 | self.find_element(By.NAME, "organization", driver=driver).find_element( 219 | By.XPATH, '//option[text()="default"]' 220 | ).click() 221 | self.find_element(By.NAME, "name", driver=driver).send_keys(location_name) 222 | self.find_element(By.NAME, "type", driver=driver).find_element( 223 | By.XPATH, '//option[@value="outdoor"]' 224 | ).click() 225 | self.find_element(By.NAME, "is_mobile", driver=driver).click() 226 | self._ignore_location_alert(driver) 227 | self._click_save_btn(driver) 228 | self.get_resource(location_name, "/admin/geo/location/", driver=driver) 229 | self._wait_until_page_ready() 230 | self.objects_to_delete.append(driver.current_url) 231 | self.open("/admin/geo/location/", driver=driver) 232 | self._wait_until_page_ready() 233 | 234 | def add_mobile_location_point(self, location_name, driver=None): 235 | """Add a point on the map for an existing mobile location. 236 | 237 | Parameters: 238 | 239 | - location_name (str): The name of the location. 240 | - driver (selenium.webdriver, optional): The Selenium WebDriver 241 | instance. Defaults to `self.base_driver`. 242 | """ 243 | if not driver: 244 | driver = self.base_driver 245 | self.get_resource(location_name, "/admin/geo/location/", driver=driver) 246 | self.find_element(By.NAME, "is_mobile", driver=driver).click() 247 | self._ignore_location_alert(driver) 248 | self.find_element( 249 | By.CLASS_NAME, "leaflet-draw-draw-marker", driver=driver 250 | ).click() 251 | self.find_element(By.ID, "id_geometry-map", driver=driver).click() 252 | self.find_element(By.NAME, "is_mobile", driver=driver).click() 253 | self._ignore_location_alert(driver) 254 | self._click_save_btn(driver) 255 | self.get_resource(location_name, "/admin/geo/location/", driver=driver) 256 | 257 | def docker_compose_get_container_id(self, container_name): 258 | """Get the Docker container ID for a specific container. 259 | 260 | Parameters: 261 | 262 | - container_name (str): The name of the Docker container. 263 | 264 | Returns: 265 | str: The ID of the Docker container. 266 | """ 267 | services_output = subprocess.Popen( 268 | ["docker", "compose", "ps", "--quiet", container_name], 269 | stdout=subprocess.PIPE, 270 | stderr=subprocess.PIPE, 271 | cwd=self.root_location, 272 | ) 273 | output, _ = services_output.communicate() 274 | return output.rstrip().decode("utf-8") 275 | 276 | def create_network_topology( 277 | self, 278 | label="automated-selenium-test-01", 279 | topology_url=( 280 | "https://raw.githubusercontent.com/openwisp/" 281 | "docker-openwisp/master/tests/static/network-graph.json" 282 | ), 283 | driver=None, 284 | ): 285 | """Create a new network topology resource. 286 | 287 | Parameters: 288 | 289 | - label (str, optional): The label for the new topology. Defaults 290 | to 'automated-selenium-test-01'. 291 | - topology_url (str, optional): The URL to fetch the topology data 292 | from. Defaults to the provided URL. 293 | - driver (selenium.webdriver, optional): The Selenium WebDriver 294 | instance. Defaults to `self.base_driver`. 295 | """ 296 | if not driver: 297 | driver = self.base_driver 298 | self.open("/admin/topology/topology/add/", driver=driver) 299 | self.find_element(By.NAME, "label", driver=driver).send_keys(label) 300 | # We can leave the organization empty for creating shared object 301 | self.find_element(By.NAME, "parser", driver=driver).find_element( 302 | By.XPATH, '//option[text()="NetJSON NetworkGraph"]' 303 | ).click() 304 | self.find_element(By.NAME, "url", driver=driver).send_keys(topology_url) 305 | self._click_save_btn(driver) 306 | self.get_resource( 307 | label, "/admin/topology/topology/", "field-label", driver=driver 308 | ) 309 | self._wait_until_page_ready() 310 | self.objects_to_delete.append(driver.current_url) 311 | self._wait_until_page_ready() 312 | --------------------------------------------------------------------------------