├── tests ├── __init__.py ├── openwisp2 │ ├── sample_geo │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_default_group_permissions.py │ │ │ └── 0003_alter_devicelocation_floorplan_location.py │ │ ├── admin.py │ │ ├── pytest.py │ │ ├── apps.py │ │ ├── models.py │ │ └── tests.py │ ├── sample_pki │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0002_default_group_permissions.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── tests.py │ │ └── models.py │ ├── sample_config │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0005_add_organizationalloweddevice.py │ │ │ ├── 0003_name_unique_per_organization.py │ │ │ ├── 0002_default_groups_permissions.py │ │ │ ├── 0004_devicegroup_templates.py │ │ │ ├── 0006_device__is_deactivated_alter_config_status.py │ │ │ └── 0007_alter_config_status.py │ │ ├── apps.py │ │ ├── pytest.py │ │ ├── admin.py │ │ └── fixtures │ │ │ └── test_templates.json │ ├── sample_users │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0004_default_groups.py │ │ ├── apps.py │ │ ├── admin.py │ │ ├── models.py │ │ └── tests.py │ ├── sample_connection │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ └── views.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_default_group_permissions.py │ │ │ └── 0003_name_unique_per_organization.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── pytest.py │ │ └── models.py │ ├── sample_subnet_division │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0002_default_group_permissions.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ └── tests.py │ ├── __init__.py │ ├── celery.py │ ├── local_settings.example.py │ └── asgi.py ├── media │ ├── .gitignore │ └── floorplan.jpg ├── manage.py └── docker-entrypoint.sh ├── openwisp_controller ├── config │ ├── __init__.py │ ├── api │ │ └── __init__.py │ ├── base │ │ ├── __init__.py │ │ └── tag.py │ ├── sortedm2m │ │ ├── __init__.py │ │ └── forms.py │ ├── controller │ │ ├── __init__.py │ │ └── urls.py │ ├── static │ │ ├── support.css │ │ ├── config │ │ │ ├── css │ │ │ │ ├── devicegroup.css │ │ │ │ └── device-delete-confirmation.css │ │ │ └── js │ │ │ │ ├── device-delete-confirmation.js │ │ │ │ ├── switcher.js │ │ │ │ └── management_ip.js │ │ ├── sortedm2m │ │ │ └── patch_sortedm2m.js │ │ └── import_export │ │ │ └── import-openwisp.css │ ├── urls.py │ ├── templates │ │ ├── admin │ │ │ ├── import_export │ │ │ │ └── import.html │ │ │ ├── config │ │ │ │ ├── change_list_device.html │ │ │ │ ├── preview.html │ │ │ │ ├── jsonschema-widget.html │ │ │ │ ├── system_context.html │ │ │ │ ├── device_recover_form.html │ │ │ │ ├── clone_template_form.html │ │ │ │ ├── change_device_group.html │ │ │ │ └── device │ │ │ │ │ └── change_form.html │ │ │ └── device_group │ │ │ │ └── change_form.html │ │ └── reversion │ │ │ └── config │ │ │ └── revision_form.html │ ├── tests │ │ ├── __init__.py │ │ ├── test_tag.py │ │ └── test_handlers.py │ ├── migrations │ │ ├── 0020_remove_config_organization.py │ │ ├── 0015_default_groups_permissions.py │ │ ├── 0031_update_vpn_dh_param.py │ │ ├── 0050_alter_vpnclient_unique_together.py │ │ ├── 0017_template_name_organization_unique_together.py │ │ ├── 0053_vpnclient_secret.py │ │ ├── 0054_device__is_deactivated.py │ │ ├── 0047_add_organizationlimits.py │ │ ├── 0007_simplify_config.py │ │ ├── 0022_vpn_format_dh.py │ │ ├── 0026_hardware_id_not_unique.py │ │ ├── 0035_device_name_unique_optional.py │ │ ├── 0014_device_hardware_id.py │ │ ├── 0006_config_device_not_null.py │ │ ├── 0041_default_groups_organizationconfigsettings_permission.py │ │ ├── 0032_update_legacy_vpn_backend.py │ │ ├── 0024_update_context_data.py │ │ ├── 0044_config_error_reason.py │ │ ├── 0009_device_system.py │ │ ├── 0052_vpn_node_network_id.py │ │ ├── 0058_alter_vpnclient_template.py │ │ ├── 0056_vpnclient_template.py │ │ ├── 0057_populate_vpnclient_template.py │ │ ├── 0033_name_unique_per_organization.py │ │ ├── 0045_alter_vpn_webhook_endpoint.py │ │ ├── 0034_template_required.py │ │ ├── 0038_vpn_subnet.py │ │ ├── 0059_zerotier_templates_ow_zt_to_global.py │ │ ├── 0023_update_context.py │ │ ├── 0012_auto_20180219_1501.py │ │ ├── 0018_config_context.py │ │ ├── 0051_organizationconfigsettings_context.py │ │ ├── 0010_auto_20180106_1814.py │ │ ├── 0049_devicegroup_context.py │ │ ├── 0011_update_device_mac_address.py │ │ ├── 0021_vpn_key.py │ │ ├── 0028_template_default_values.py │ │ ├── 0025_update_device_key.py │ │ ├── 0005_populate_device.py │ │ ├── 0016_default_organization_config_settings.py │ │ ├── 0037_alter_taggedtemplate.py │ │ ├── 0060_cleanup_api_task_notification_types.py │ │ ├── 0043_devicegroup_templates.py │ │ ├── 0040_vpnclient_ip_setnull.py │ │ ├── 0027_add_indexes_on_ip_fields.py │ │ ├── 0030_django_taggit_update.py │ │ ├── 0055_alter_config_status.py │ │ ├── 0029_merge_django_netjsonconfig.py │ │ └── 0046_organizationlimits.py │ ├── exceptions.py │ ├── crypto.py │ ├── validators.py │ ├── filters.py │ └── fixtures │ │ └── test_templates.json ├── geo │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── urls.py │ │ └── filters.py │ ├── base │ │ └── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_apps.py │ │ └── test_models.py │ ├── channels │ │ ├── __init__.py │ │ ├── consumers.py │ │ └── routing.py │ ├── templates │ │ └── admin │ │ │ └── widgets │ │ │ └── foreign_key_raw_id.html │ ├── migrations │ │ ├── 0002_default_groups_permissions.py │ │ ├── __init__.py │ │ └── 0003_alter_devicelocation_floorplan_location.py │ ├── models.py │ └── utils.py ├── pki │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ └── models.py │ ├── tests │ │ ├── __init__.py │ │ └── utils.py │ ├── templates │ │ └── admin │ │ │ └── pki │ │ │ └── change_form.html │ ├── urls.py │ ├── static │ │ └── admin │ │ │ └── pki │ │ │ └── js │ │ │ └── show-org-field.js │ ├── models.py │ ├── migrations │ │ ├── 0007_default_groups_permissions.py │ │ ├── 0009_common_name_maxlength_64.py │ │ ├── 0002_add_organization_name.py │ │ ├── 0005_organizational_unit_name.py │ │ ├── 0006_add_x509_passphrase_field.py │ │ ├── 0008_serial_number_length.py │ │ ├── 0003_fill_organization_name.py │ │ ├── 0004_auto_20180106_1814.py │ │ └── __init__.py │ ├── utils.py │ ├── admin.py │ ├── api │ │ └── urls.py │ └── apps.py ├── connection │ ├── __init__.py │ ├── base │ │ └── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── test-key.ed25519 │ │ └── test-key.rsa │ ├── connectors │ │ ├── __init__.py │ │ ├── openwrt │ │ │ ├── __init__.py │ │ │ └── snmp.py │ │ ├── airos │ │ │ └── snmp.py │ │ ├── exceptions.py │ │ └── snmp.py │ ├── utils.py │ ├── schema.py │ ├── signals.py │ ├── exceptions.py │ ├── channels │ │ ├── routing.py │ │ └── consumers.py │ ├── migrations │ │ ├── 0003_default_group_permissions.py │ │ ├── 0004_django3_1_upgrade.py │ │ ├── 0009_alter_deviceconnection_unique_together.py │ │ ├── 0006_name_unique_per_organization.py │ │ ├── 0002_credentials_auto_add.py │ │ ├── 0005_device_connection_failure_reason.py │ │ └── 0008_remove_conflicting_deviceconnections.py │ ├── static │ │ └── connection │ │ │ ├── js │ │ │ └── credentials.js │ │ │ └── css │ │ │ └── credentials.css │ ├── models.py │ ├── api │ │ └── urls.py │ └── widgets.py ├── subnet_division │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_rule.py │ ├── rule_types │ │ └── __init__.py │ ├── signals.py │ ├── static │ │ └── subnet-division │ │ │ └── css │ │ │ └── subnet-division.css │ ├── migrations │ │ ├── 0002_default_group_migration.py │ │ ├── 0004_index_rule_on_delete.py │ │ ├── __init__.py │ │ ├── 0005_number_of_subnets_and_ips.py │ │ └── 0003_related_field_allow_blank.py │ ├── models.py │ ├── settings.py │ └── utils.py ├── settings.py ├── context_processors.py ├── tests │ ├── test_users_integration.py │ ├── __init__.py │ ├── utils.py │ └── mixins.py ├── routing.py ├── serializers.py ├── migrations.py ├── __init__.py ├── admin.py ├── mixins.py └── base.py ├── .prettierignore ├── CONTRIBUTING.rst ├── docs ├── images │ └── architecture-v2-openwisp-controller.png ├── developer │ └── index.rst ├── partials │ ├── shared-object.rst │ └── developer-docs.rst ├── user │ ├── organization-limits.rst │ ├── import-export.rst │ └── device-config-status.rst └── index.rst ├── pytest.ini ├── requirements-test.txt ├── setup.cfg ├── SECURITY.md ├── MANIFEST.in ├── .github ├── workflows │ ├── version-branch.yml │ ├── publiccode-yml-validation.yml │ └── pypi.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md ├── FUNDING.yml └── dependabot.yml ├── docker-compose.yml ├── runtests ├── pyproject.toml ├── Dockerfile ├── requirements.txt ├── .gitignore ├── run-qa-checks └── runtests.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/geo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/pki/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_geo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/config/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/config/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/connection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/geo/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/geo/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/geo/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/pki/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/pki/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/config/sortedm2m/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/connection/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/connection/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/geo/channels/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/config/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/connection/connectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_config/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_geo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_subnet_division/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/rule_types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openwisp_controller/connection/connectors/openwrt/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/media/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !floorplan.jpg 4 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_subnet_division/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_geo/admin.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.geo import admin # noqa 2 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/admin.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.pki import admin # noqa 2 | -------------------------------------------------------------------------------- /tests/openwisp2/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ["celery_app"] 4 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/admin.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.connection import admin # noqa 2 | -------------------------------------------------------------------------------- /openwisp_controller/pki/templates/admin/pki/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/django_x509/change_form.html" %} 2 | -------------------------------------------------------------------------------- /tests/media/floorplan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/openwisp-controller/HEAD/tests/media/floorplan.jpg -------------------------------------------------------------------------------- /openwisp_controller/geo/templates/admin/widgets/foreign_key_raw_id.html: -------------------------------------------------------------------------------- 1 | {% include 'admin/django_loci/foreign_key_raw_id.html' %} 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | openwisp_controller/config/static/config/js/lib/*.js 2 | openwisp_controller/connection/static/connection/js/lib/*.js 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Please refer to the `OpenWISP Contribution Guidelines 2 | `_. 3 | -------------------------------------------------------------------------------- /openwisp_controller/connection/connectors/airos/snmp.py: -------------------------------------------------------------------------------- 1 | from ..snmp import Snmp as BaseSnmp 2 | 3 | 4 | class AirOsSnmp(BaseSnmp): 5 | pass 6 | -------------------------------------------------------------------------------- /openwisp_controller/connection/connectors/openwrt/snmp.py: -------------------------------------------------------------------------------- 1 | from ..snmp import Snmp as BaseSnmp 2 | 3 | 4 | class OpenWRTSnmp(BaseSnmp): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_subnet_division/admin.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.subnet_division.admin import ( # noqa 2 | SubnetDivisionRuleInlineAdmin, 3 | ) 4 | -------------------------------------------------------------------------------- /docs/images/architecture-v2-openwisp-controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/openwisp-controller/HEAD/docs/images/architecture-v2-openwisp-controller.png -------------------------------------------------------------------------------- /openwisp_controller/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | OPENWISP_CONTROLLER_API_HOST = getattr(settings, "OPENWISP_CONTROLLER_API_HOST", None) 4 | -------------------------------------------------------------------------------- /openwisp_controller/connection/tests/base.py: -------------------------------------------------------------------------------- 1 | # pragma: no cover 2 | # kept for backward compatibility with previous versions 3 | from .utils import CreateConnectionsMixin, SshServer # noqa 4 | -------------------------------------------------------------------------------- /openwisp_controller/geo/api/urls.py: -------------------------------------------------------------------------------- 1 | from ..utils import get_geo_urls 2 | from . import views as geo_views 3 | 4 | app_name = "openwisp_controller" 5 | 6 | urlpatterns = get_geo_urls(geo_views) 7 | -------------------------------------------------------------------------------- /openwisp_controller/config/controller/urls.py: -------------------------------------------------------------------------------- 1 | from ..utils import get_controller_urls 2 | from . import views 3 | 4 | app_name = "openwisp_controller" 5 | urlpatterns = get_controller_urls(views) 6 | -------------------------------------------------------------------------------- /openwisp_controller/config/static/support.css: -------------------------------------------------------------------------------- 1 | .help.icon { 2 | mask-image: url(http://localhost:8000/media/support.png); 3 | -webkit-mask-image: url(http://localhost:8000/media/support.png); 4 | } 5 | -------------------------------------------------------------------------------- /openwisp_controller/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import schema 4 | 5 | app_name = "openwisp_controller" 6 | urlpatterns = [path("config/schema.json", schema, name="schema")] 7 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | subnet_provisioned = Signal() 4 | subnet_provisioned.__doc__ = """ 5 | Providing arguments: ['instance', 'provisioned'] 6 | """ 7 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -p no:warnings --create-db --reuse-db --nomigrations 3 | DJANGO_SETTINGS_MODULE = openwisp2.settings 4 | python_files = pytest*.py 5 | python_classes = *Test* 6 | pythonpath = tests 7 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_geo/pytest.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.geo.tests.pytest import TestChannels as BaseTestChannels 2 | 3 | 4 | class TestChannels(BaseTestChannels): 5 | pass 6 | 7 | 8 | del BaseTestChannels 9 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest-cov~=7.0.0 2 | openwisp-utils[qa,selenium,channels-test] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz 3 | django_redis~=6.0.0 4 | mock-ssh-server~=0.9.1 5 | responses~=0.25.8 6 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_geo/apps.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.geo.apps import GeoConfig 2 | 3 | 4 | class SampleGeoConfig(GeoConfig): 5 | name = "openwisp2.sample_geo" 6 | label = "sample_geo" 7 | 8 | 9 | del GeoConfig 10 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/apps.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.pki.apps import PkiConfig 2 | 3 | 4 | class SamplePkiConfig(PkiConfig): 5 | name = "openwisp2.sample_pki" 6 | label = "sample_pki" 7 | 8 | 9 | del PkiConfig 10 | -------------------------------------------------------------------------------- /openwisp_controller/context_processors.py: -------------------------------------------------------------------------------- 1 | from . import settings as app_settings 2 | 3 | 4 | def controller_api_settings(request): 5 | return { 6 | "OPENWISP_CONTROLLER_API_HOST": app_settings.OPENWISP_CONTROLLER_API_HOST, 7 | } 8 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_config/apps.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.config.apps import ConfigConfig 2 | 3 | 4 | class SampleConfigConfig(ConfigConfig): 5 | name = "openwisp2.sample_config" 6 | label = "sample_config" 7 | 8 | 9 | del ConfigConfig 10 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_users/apps.py: -------------------------------------------------------------------------------- 1 | from openwisp_users.apps import OpenwispUsersConfig 2 | 3 | 4 | class SampleUsersConfig(OpenwispUsersConfig): 5 | name = "openwisp2.sample_users" 6 | label = "sample_users" 7 | 8 | 9 | del OpenwispUsersConfig 10 | -------------------------------------------------------------------------------- /openwisp_controller/pki/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | There are no urls in this file. It has no functional use. 3 | This file exists only to maintain backward compatibility. 4 | """ 5 | 6 | app_name = "openwisp_controller" # pragma: no cover 7 | urlpatterns = [] # pragma: no cover 8 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_config/pytest.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.config.tests.pytest import ( 2 | TestDeviceConsumer as BaseTestDeviceConsumer, 3 | ) 4 | 5 | 6 | class TestDeviceConsumer(BaseTestDeviceConsumer): 7 | pass 8 | 9 | 10 | del BaseTestDeviceConsumer 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [flake8] 5 | # W503: line break before or after operator 6 | # W504: line break after or after operator 7 | # W605: invalid escape sequence 8 | ignore = W605, W503, W504 9 | exclude = ./tests/*settings*.py 10 | max-line-length = 88 11 | -------------------------------------------------------------------------------- /tests/openwisp2/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp2.settings") 6 | 7 | app = Celery("openwisp2") 8 | app.config_from_object("django.conf:settings", namespace="CELERY") 9 | app.autodiscover_tasks() 10 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/apps.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.connection.apps import ConnectionConfig 2 | 3 | 4 | class SampleConnectionConfig(ConnectionConfig): 5 | name = "openwisp2.sample_connection" 6 | label = "sample_connection" 7 | 8 | 9 | del ConnectionConfig 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Latest release and development version. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please write to security@openwisp.io. 10 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/pytest.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.connection.tests.pytest import ( 2 | TestCommandsConsumer as BaseTestCommandsConsumer, 3 | ) 4 | 5 | 6 | class TestCommandsConsumer(BaseTestCommandsConsumer): 7 | pass 8 | 9 | 10 | del BaseTestCommandsConsumer 11 | -------------------------------------------------------------------------------- /openwisp_controller/config/templates/admin/import_export/import.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/import_export/import.html" %} 2 | {% load static %} 3 | 4 | {% block extrastyle %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp2.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /openwisp_controller/connection/utils.py: -------------------------------------------------------------------------------- 1 | from openwisp_notifications.utils import _get_object_link 2 | 3 | 4 | def get_connection_working_notification_target_url(obj, field, absolute_url=True): 5 | url = _get_object_link(obj._related_object(field), absolute_url) 6 | return f"{url}#deviceconnection_set-group" 7 | -------------------------------------------------------------------------------- /openwisp_controller/pki/static/admin/pki/js/show-org-field.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | django.jQuery(function ($) { 3 | var showField = function () { 4 | $(".form-row.field-organization").show(); 5 | }; 6 | $(".field-operation_type select").on("change", function () { 7 | showField(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | include requirements.txt 5 | recursive-include openwisp_controller * 6 | recursive-exclude * *.pyc 7 | recursive-exclude * *.swp 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.db 10 | recursive-exclude * local_settings.py 11 | -------------------------------------------------------------------------------- /openwisp_controller/connection/schema.py: -------------------------------------------------------------------------------- 1 | from django.utils.module_loading import import_string 2 | 3 | from .settings import CONNECTORS 4 | 5 | schema = {} 6 | 7 | for connector in CONNECTORS: 8 | class_path = connector[0] 9 | class_ = import_string(class_path) 10 | schema[class_path] = class_.schema 11 | -------------------------------------------------------------------------------- /openwisp_controller/connection/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | is_working_changed = Signal() 4 | is_working_changed.__doc__ = """ 5 | Providing araguments: [ 6 | 'is_working', 7 | 'old_is_working', 8 | 'instance', 9 | 'failure_reason', 10 | 'old_failure_reason' 11 | ] 12 | """ 13 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_subnet_division/apps.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.subnet_division.apps import SubnetDivisionConfig 2 | 3 | 4 | class SampleSubnetDivisionConfig(SubnetDivisionConfig): 5 | name = "openwisp2.sample_subnet_division" 6 | label = "sample_subnet_division" 7 | 8 | 9 | del SubnetDivisionConfig 10 | -------------------------------------------------------------------------------- /.github/workflows/version-branch.yml: -------------------------------------------------------------------------------- 1 | name: Replicate Commits to Version Branch 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | version-branch: 10 | uses: openwisp/openwisp-utils/.github/workflows/reusable-version-branch.yml@master 11 | with: 12 | module_name: openwisp_controller 13 | -------------------------------------------------------------------------------- /openwisp_controller/config/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # pragma: no cover 2 | # kept for backward compatibility with previous versions 3 | from .utils import ( # noqa 4 | CreateConfigMixin, 5 | CreateConfigTemplateMixin, 6 | CreateDeviceMixin, 7 | CreateTemplateMixin, 8 | CreateVpnMixin, 9 | TestVpnX509Mixin, 10 | ) 11 | -------------------------------------------------------------------------------- /openwisp_controller/connection/exceptions.py: -------------------------------------------------------------------------------- 1 | class NoWorkingDeviceConnectionError(Exception): 2 | """ 3 | raised when none of the device's DeviceConnection 4 | are working. 5 | """ 6 | 7 | def __init__(self, connection, *args: object): 8 | self.connection = connection 9 | super().__init__(*args) 10 | -------------------------------------------------------------------------------- /docs/developer/index.rst: -------------------------------------------------------------------------------- 1 | Developer Docs 2 | ============== 3 | 4 | .. include:: ../partials/developer-docs.rst 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | ./installation.rst 10 | ./utils.rst 11 | ./extending.rst 12 | 13 | Other useful resources: 14 | 15 | - :doc:`../user/rest-api` 16 | - :doc:`../user/settings` 17 | -------------------------------------------------------------------------------- /openwisp_controller/connection/connectors/exceptions.py: -------------------------------------------------------------------------------- 1 | class CommandFailedException(Exception): 2 | """ 3 | raised when a command returns an unexpected result 4 | """ 5 | 6 | pass 7 | 8 | 9 | class CommandTimeoutException(Exception): 10 | """ 11 | raised when a command times out 12 | """ 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /.github/workflows/publiccode-yml-validation.yml: -------------------------------------------------------------------------------- 1 | name: publiccode.yml validation 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | publiccode_yml_validation: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v5 10 | - uses: italia/publiccode-parser-action@v1 11 | with: 12 | publiccode: "publiccode.yml" 13 | -------------------------------------------------------------------------------- /openwisp_controller/connection/channels/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import consumers as ow_consumer 4 | 5 | 6 | def get_routes(consumer=ow_consumer): 7 | return [ 8 | re_path( 9 | r"^ws/controller/device/(?P[^/]+)/command$", 10 | consumer.CommandConsumer.as_asgi(), 11 | ) 12 | ] 13 | -------------------------------------------------------------------------------- /openwisp_controller/config/static/config/css/devicegroup.css: -------------------------------------------------------------------------------- 1 | #id_meta_data_jsoneditor { 2 | min-height: 75px; 3 | } 4 | #id_meta_data_jsoneditor > div[data-schemaid="root"] { 5 | margin-top: 15px; 6 | } 7 | .jsoneditor-wrapper div[data-schemaid="root"] > label:first-of-type, 8 | .jsoneditor-wrapper div[data-schemaid="root"] > select:first-of-type { 9 | position: static; 10 | } 11 | -------------------------------------------------------------------------------- /.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 | Please use the [Discussion Forum](https://github.com/orgs/openwisp/discussions) to ask questions. 10 | 11 | We will take care of moving the discussion to a more relevant repository if needed. 12 | -------------------------------------------------------------------------------- /docs/partials/shared-object.rst: -------------------------------------------------------------------------------- 1 | .. note:: 2 | 3 | This guide creates the VPN server and VPN client templates as **Shared 4 | systemwide (no organization)** objects. This allows any device of any 5 | organization to use the automation. 6 | 7 | If needed, you can use any organization as long as the VPN server, the 8 | VPN client template, and devices have the same organization. 9 | -------------------------------------------------------------------------------- /openwisp_controller/tests/test_users_integration.py: -------------------------------------------------------------------------------- 1 | from openwisp_users.tests.test_admin import TestUsersAdmin 2 | 3 | from .mixins import GetEditFormInlineMixin 4 | 5 | 6 | class TestUsersIntegration(GetEditFormInlineMixin, TestUsersAdmin): 7 | """ 8 | tests integration with openwisp_users 9 | """ 10 | 11 | is_integration_test = True 12 | 13 | 14 | del TestUsersAdmin 15 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_config/admin.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.config.admin import ( 2 | DeviceAdmin, 3 | DeviceGroupAdmin, 4 | TemplateAdmin, 5 | VpnAdmin, 6 | ) 7 | 8 | # Monkey Patching done only for testing purposes 9 | DeviceAdmin.fields += ["details"] 10 | TemplateAdmin.fields += ["details"] 11 | VpnAdmin.fields += ["details"] 12 | DeviceGroupAdmin.fields += ["details"] 13 | -------------------------------------------------------------------------------- /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:`OpenWISP Controller User Docs ` 10 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/tests.py: -------------------------------------------------------------------------------- 1 | from openwisp_controller.pki.tests.test_admin import TestAdmin as BaseTestAdmin 2 | from openwisp_controller.pki.tests.test_models import TestModels as BaseTestModels 3 | 4 | 5 | class TestAdmin(BaseTestAdmin): 6 | app_label = "sample_pki" 7 | 8 | 9 | class TestModels(BaseTestModels): 10 | pass 11 | 12 | 13 | del BaseTestAdmin 14 | del BaseTestModels 15 | -------------------------------------------------------------------------------- /openwisp_controller/config/static/config/js/device-delete-confirmation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | (function ($) { 4 | $(document).ready(function () { 5 | $("#warning-ack").click(function (event) { 6 | $("#deactivating-warning").slideUp("fast"); 7 | $("#delete-confirm-container").slideDown("fast"); 8 | $('input[name="force_delete"]').val("true"); 9 | }); 10 | }); 11 | })(django.jQuery); 12 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/static/subnet-division/css/subnet-division.css: -------------------------------------------------------------------------------- 1 | input.readonly { 2 | border: 1px solid rgba(0, 0, 0, 0.05) !important; 3 | background-color: rgba(0, 0, 0, 0.07); 4 | } 5 | .help-text-warning { 6 | background-color: #ffe5e5; 7 | padding: 5px 10px; 8 | font-size: 15px; 9 | font-weight: bolder; 10 | display: flex; 11 | } 12 | .help-text-warning img { 13 | min-width: 30px; 14 | } 15 | -------------------------------------------------------------------------------- /tests/openwisp2/local_settings.example.py: -------------------------------------------------------------------------------- 1 | # RENAME THIS FILE TO local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS 2 | # BUT DO NOT COMMIT 3 | 4 | # DATABASES = { 5 | # 'default': { 6 | # 'ENGINE': 'openwisp_utils.db.backends.spatialite', 7 | # 'NAME': 'openwisp-controller.db', 8 | # 'USER': '', 9 | # 'PASSWORD': '', 10 | # 'HOST': '', 11 | # 'PORT': '' 12 | # }, 13 | # } 14 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0020_remove_config_organization.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.10 on 2019-02-09 07:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0019_organization_mac_add_hardware_id_name_unique_together") 9 | ] 10 | 11 | operations = [migrations.RemoveField(model_name="config", name="organization")] 12 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/migrations/0002_default_group_migration.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from . import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("subnet_division", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.RunPython( 11 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /openwisp_controller/connection/migrations/0003_default_group_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from . import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("connection", "0002_credentials_auto_add")] 8 | 9 | operations = [ 10 | migrations.RunPython( 11 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /openwisp_controller/connection/channels/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | 4 | from swapper import load_model 5 | 6 | from ...config.base.channels_consumer import BaseDeviceConsumer 7 | 8 | Device = load_model("config", "Device") 9 | 10 | 11 | class CommandConsumer(BaseDeviceConsumer): 12 | def send_update(self, event): 13 | data = deepcopy(event) 14 | data.pop("type") 15 | self.send(json.dumps(data)) 16 | -------------------------------------------------------------------------------- /openwisp_controller/geo/migrations/0002_default_groups_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from . import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("geo", "0001_initial"), 9 | ] 10 | operations = [ 11 | migrations.RunPython( 12 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /openwisp_controller/connection/tests/test-key.ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACBSjdrpn90sb9ydtqddFY4PZDBpe5YFxGYhqx4OM+dBTgAAAJBLNjRpSzY0 4 | aQAAAAtzc2gtZWQyNTUxOQAAACBSjdrpn90sb9ydtqddFY4PZDBpe5YFxGYhqx4OM+dBTg 5 | AAAEAgZ6vvYLFXzDzPCCZ4+dsc6PG9IJg/3W5oeoXy51HVuFKN2umf3Sxv3J22p10Vjg9k 6 | MGl7lgXEZiGrHg4z50FOAAAADG5lbWVzaXNAZW52eQE= 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /openwisp_controller/pki/models.py: -------------------------------------------------------------------------------- 1 | import swapper 2 | 3 | from .base.models import AbstractCa, AbstractCert 4 | 5 | 6 | class Ca(AbstractCa): 7 | class Meta(AbstractCa.Meta): 8 | abstract = False 9 | swappable = swapper.swappable_setting("django_x509", "Ca") 10 | 11 | 12 | class Cert(AbstractCert): 13 | class Meta(AbstractCert.Meta): 14 | abstract = False 15 | swappable = swapper.swappable_setting("django_x509", "Cert") 16 | -------------------------------------------------------------------------------- /openwisp_controller/routing.py: -------------------------------------------------------------------------------- 1 | from openwisp_notifications.websockets.routing import ( 2 | get_routes as get_notification_routes, 3 | ) 4 | 5 | from openwisp_controller.connection.channels.routing import ( 6 | get_routes as get_connection_routes, 7 | ) 8 | from openwisp_controller.geo.channels.routing import get_routes as get_geo_routes 9 | 10 | 11 | def get_routes(): 12 | return get_geo_routes() + get_connection_routes() + get_notification_routes() 13 | -------------------------------------------------------------------------------- /openwisp_controller/serializers.py: -------------------------------------------------------------------------------- 1 | from openwisp_users.api.mixins import FilterSerializerByOrgManaged 2 | from openwisp_utils.api.serializers import ValidatedModelSerializer 3 | 4 | 5 | class BaseSerializer(FilterSerializerByOrgManaged, ValidatedModelSerializer): 6 | """BaseSerializer for most API endpoints. 7 | 8 | - FilterSerializerByOrgManaged: for multi-tenancy 9 | - ValidatedModelSerializer: for model validation 10 | """ 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_geo/migrations/0002_default_group_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from openwisp_controller.geo.migrations import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("sample_geo", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.RunPython( 11 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/migrations/0002_default_group_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from openwisp_controller.pki.migrations import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("sample_pki", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.RunPython( 11 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /openwisp_controller/pki/migrations/0007_default_groups_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from . import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("pki", "0006_add_x509_passphrase_field"), 9 | ] 10 | operations = [ 11 | migrations.RunPython( 12 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_users/admin.py: -------------------------------------------------------------------------------- 1 | from openwisp_users import admin 2 | from openwisp_users.utils import ( 3 | usermodel_add_form, 4 | usermodel_change_form, 5 | usermodel_list_and_search, 6 | ) 7 | 8 | additional_fields = [ 9 | [2, "social_security_number"], 10 | ] 11 | 12 | usermodel_add_form(admin.UserAdmin, additional_fields) 13 | usermodel_change_form(admin.UserAdmin, additional_fields) 14 | usermodel_list_and_search(admin.UserAdmin, additional_fields) 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # NOTE: This Docker image is for development purposes only. 2 | 3 | services: 4 | controller: 5 | image: openwisp/controller-development:latest 6 | environment: 7 | - REDIS_URL=redis://redis:6379 8 | build: 9 | context: . 10 | ports: 11 | - 8000:8000 12 | depends_on: 13 | - redis 14 | 15 | redis: 16 | image: redis:alpine 17 | ports: 18 | - "6379:6379" 19 | entrypoint: redis-server --appendonly yes 20 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0015_default_groups_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from . import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0014_device_hardware_id"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RunPython( 13 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0031_update_vpn_dh_param.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-10 07:25 2 | 3 | from django.db import migrations 4 | 5 | from . import update_vpn_dhparam_length 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [("config", "0030_django_taggit_update")] 10 | 11 | operations = [ 12 | migrations.RunPython( 13 | update_vpn_dhparam_length, reverse_code=migrations.RunPython.noop 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0050_alter_vpnclient_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.3 on 2023-07-18 16:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0049_devicegroup_context"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name="vpnclient", 14 | unique_together={("config", "vpn")}, 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_subnet_division/migrations/0002_default_group_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from openwisp_controller.subnet_division.migrations import assign_permissions_to_groups 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("sample_subnet_division", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.RunPython( 11 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0017_template_name_organization_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.9 on 2018-12-08 05:22 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0016_default_organization_config_settings"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name="template", unique_together={("organization", "name")} 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0053_vpnclient_secret.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-07-27 12:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0052_vpn_node_network_id"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="vpnclient", 14 | name="secret", 15 | field=models.TextField(blank=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0054_device__is_deactivated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2024-02-29 11:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0053_vpnclient_secret"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="device", 14 | name="_is_deactivated", 15 | field=models.BooleanField(default=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /openwisp_controller/config/templates/admin/config/change_list_device.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n admin_urls %} 3 | 4 | {% block object-tools-items %} 5 | {% if not is_popup and has_add_permission and has_change_permission %} 6 |
  • {% blocktrans with cl.opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}
  • 7 | {% endif %} 8 | {{ block.super }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /openwisp_controller/config/templates/admin/config/preview.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_urls %} 3 | 4 | {% block content %} 5 |
    6 |
    7 | × {% trans 'Close' %} 8 | {% if not error %} 9 |
    {{ output }}
    10 | {% else %} 11 |
    {{ error }}
    12 | {% endif %} 13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0047_add_organizationlimits.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-09-16 11:45 2 | 3 | 4 | from django.db import migrations 5 | 6 | from . import populate_organization_allowed_device 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("config", "0046_organizationlimits"), 12 | ] 13 | 14 | operations = [ 15 | migrations.RunPython( 16 | populate_organization_allowed_device, migrations.RunPython.noop 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/config/static/sortedm2m/patch_sortedm2m.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | (function ($) { 3 | $(document).ready(function () { 4 | if ($(".sortedm2m-items").length < 2) { 5 | destroyHiddenSortableWidget($); 6 | } 7 | $(document).on("click", "a.inline-deletelink", function () { 8 | destroyHiddenSortableWidget($); 9 | }); 10 | }); 11 | 12 | function destroyHiddenSortableWidget($) { 13 | $(".inline-related.empty-form .sortedm2m-items").sortable("destroy"); 14 | } 15 | })(django.jQuery); 16 | -------------------------------------------------------------------------------- /openwisp_controller/connection/migrations/0004_django3_1_upgrade.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-08-18 11:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("connection", "0003_default_group_permissions")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="deviceconnection", 12 | name="is_working", 13 | field=models.BooleanField(blank=True, default=None, null=True), 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0007_simplify_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-05-11 18:41 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("config", "0006_config_device_not_null")] 8 | 9 | operations = [ 10 | migrations.RemoveField(model_name="config", name="name"), 11 | migrations.RemoveField(model_name="config", name="key"), 12 | migrations.RemoveField(model_name="config", name="mac_address"), 13 | ] 14 | -------------------------------------------------------------------------------- /openwisp_controller/config/templates/admin/config/jsonschema-widget.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if advanced_mode %} 3 | 4 | {% endif %} 5 | {% if netjsonconfig_hint %} 6 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /tests/openwisp2/asgi.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 | 6 | from openwisp_controller.routing import get_routes 7 | 8 | application = ProtocolTypeRouter( 9 | { 10 | "websocket": AllowedHostsOriginValidator( 11 | AuthMiddlewareStack(URLRouter(get_routes())) 12 | ), 13 | "http": get_asgi_application(), 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_pki/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from openwisp_controller.pki.base.models import AbstractCa, AbstractCert 4 | 5 | 6 | class DetailsModel(models.Model): 7 | details = models.CharField(max_length=64, blank=True, null=True) 8 | 9 | class Meta: 10 | abstract = True 11 | 12 | 13 | class Ca(DetailsModel, AbstractCa): 14 | class Meta(AbstractCa.Meta): 15 | abstract = False 16 | 17 | 18 | class Cert(DetailsModel, AbstractCert): 19 | class Meta(AbstractCert.Meta): 20 | abstract = False 21 | -------------------------------------------------------------------------------- /runtests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Starting standard tests" 5 | coverage run runtests.py --parallel --exclude-tag=selenium_tests \ 6 | || coverage run ./runtests.py 7 | 8 | echo "Starting standard tests" 9 | coverage run runtests.py --tag=selenium_tests --no-input --exclude-pytest 10 | 11 | echo "Starting tests for extensibility" 12 | SAMPLE_APP=1 coverage run ./runtests.py \ 13 | --parallel --exclude-tag=selenium_tests \ 14 | || SAMPLE_APP=1 coverage run ./runtests.py \ 15 | --exclude-tag=selenium_tests 16 | 17 | coverage combine 18 | coverage xml 19 | -------------------------------------------------------------------------------- /openwisp_controller/config/static/config/css/device-delete-confirmation.css: -------------------------------------------------------------------------------- 1 | #deactivating-warning .warning p { 2 | margin-top: 0px; 3 | } 4 | #deactivating-warning .messagelist button { 5 | font-size: 15px; 6 | vertical-align: middle; 7 | line-height: inherit !important; 8 | padding: 0.625rem 1rem; 9 | } 10 | #deactivating-warning .messagelist button + button { 11 | margin-left: 10px; 12 | } 13 | #main ul.messagelist li.warning ul li { 14 | display: list-item; 15 | padding: 0px; 16 | background: inherit; 17 | } 18 | ul.messagelist li { 19 | font-size: unset; 20 | } 21 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_config/migrations/0005_add_organizationalloweddevice.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-09-16 11:45 2 | 3 | 4 | from django.db import migrations 5 | 6 | from openwisp_controller.config.migrations import populate_organization_allowed_device 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("sample_config", "0004_devicegroup_templates"), 12 | ] 13 | 14 | operations = [ 15 | migrations.RunPython( 16 | populate_organization_allowed_device, migrations.RunPython.noop 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0022_vpn_format_dh.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def format_dh(apps, schema_editor): 5 | Vpn = apps.get_model("config", "Vpn") 6 | 7 | for vpn in Vpn.objects.all(): 8 | if vpn.dh.startswith("b'") and vpn.dh.endswith("'"): 9 | vpn.dh = vpn.dh[2:-1] 10 | vpn.save() 11 | 12 | 13 | class Migration(migrations.Migration): 14 | dependencies = [("config", "0021_vpn_key")] 15 | 16 | operations = [ 17 | migrations.RunPython(format_dh, reverse_code=migrations.RunPython.noop) 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0026_hardware_id_not_unique.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-03-13 23:45 2 | 3 | from django.db import migrations, models 4 | 5 | from openwisp_controller.config import settings as app_settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [("config", "0025_update_device_key")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="device", 14 | name="hardware_id", 15 | field=models.CharField(**app_settings.HARDWARE_ID_OPTIONS), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0035_device_name_unique_optional.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.8 on 2021-05-04 20:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("config", "0034_template_required"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name="device", 14 | unique_together={ 15 | ("mac_address", "organization"), 16 | ("hardware_id", "organization"), 17 | }, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /openwisp_controller/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from openwisp_utils.utils import deepcopy 4 | 5 | 6 | def _get_updated_templates_settings(context_processors=[]): 7 | template_settings = deepcopy(settings.TEMPLATES[0]) 8 | if len(context_processors): 9 | template_settings["OPTIONS"]["context_processors"].extend(context_processors) 10 | else: 11 | template_settings["OPTIONS"]["context_processors"].append( 12 | "openwisp_controller.context_processors.controller_api_settings" 13 | ) 14 | return [template_settings] 15 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0014_device_hardware_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.9 on 2018-10-25 14:06 2 | 3 | from django.db import migrations, models 4 | 5 | from openwisp_controller.config import settings as app_settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [("config", "0013_last_ip_management_ip_and_status_applied")] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="device", 14 | name="hardware_id", 15 | field=models.CharField(**(app_settings.HARDWARE_ID_OPTIONS)), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.coverage.run] 2 | source = ["openwisp_controller"] 3 | parallel = true 4 | concurrency = ["multiprocessing", "thread"] 5 | omit = [ 6 | "openwisp_controller/__init__.py", 7 | "*/tests/*", 8 | "*/migrations/*", 9 | ] 10 | 11 | [tool.docstrfmt] 12 | extend_exclude = ["**/*.py"] 13 | 14 | [tool.isort] 15 | known_third_party = ["django", "django_x509"] 16 | known_first_party = ["openwisp_users", "openwisp_utils"] 17 | default_section = "THIRDPARTY" 18 | line_length = 88 19 | multi_line_output = 3 20 | use_parentheses = true 21 | include_trailing_comma = true 22 | force_grid_wrap = 0 23 | -------------------------------------------------------------------------------- /openwisp_controller/config/tests/test_tag.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from swapper import load_model 3 | 4 | from openwisp_users.tests.utils import TestOrganizationMixin 5 | 6 | from .utils import CreateTemplateMixin 7 | 8 | Template = load_model("config", "Template") 9 | 10 | 11 | class TestTag(TestOrganizationMixin, CreateTemplateMixin, TestCase): 12 | """ 13 | tests for Tag model 14 | """ 15 | 16 | def test_tag(self): 17 | t = self._create_template(organization=self._get_org()) 18 | t.tags.add("mesh") 19 | self.assertEqual(t.tags.filter(name="mesh").count(), 1) 20 | -------------------------------------------------------------------------------- /openwisp_controller/migrations.py: -------------------------------------------------------------------------------- 1 | # Used in migrations of apps 2 | import swapper 3 | from django.contrib.auth.management import create_permissions 4 | 5 | 6 | def create_default_permissions(apps, schema_editor): 7 | for app_config in apps.get_app_configs(): 8 | app_config.models_module = True 9 | create_permissions(app_config, apps=apps, verbosity=0) 10 | app_config.models_module = None 11 | 12 | 13 | def get_swapped_model(apps, app_name, model_name): 14 | model_path = swapper.get_model_name(app_name, model_name) 15 | app, model = swapper.split(model_path) 16 | return apps.get_model(app, model) 17 | -------------------------------------------------------------------------------- /openwisp_controller/subnet_division/models.py: -------------------------------------------------------------------------------- 1 | from swapper import swappable_setting 2 | 3 | from .base.models import AbstractSubnetDivisionIndex, AbstractSubnetDivisionRule 4 | 5 | 6 | class SubnetDivisionRule(AbstractSubnetDivisionRule): 7 | class Meta(AbstractSubnetDivisionRule.Meta): 8 | abstract = False 9 | swappable = swappable_setting("subnet_division", "SubnetDivisionRule") 10 | 11 | 12 | class SubnetDivisionIndex(AbstractSubnetDivisionIndex): 13 | class Meta(AbstractSubnetDivisionIndex.Meta): 14 | abstract = False 15 | swappable = swappable_setting("subnet_division", "SubnetDivisionIndex") 16 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0006_config_device_not_null.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-05-11 18:35 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [("config", "0005_populate_device")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="config", 13 | name="device", 14 | field=models.OneToOneField( 15 | on_delete=django.db.models.deletion.CASCADE, to="config.Device" 16 | ), 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/pki/tests/utils.py: -------------------------------------------------------------------------------- 1 | from django_x509.tests import TestX509Mixin 2 | from swapper import load_model 3 | 4 | 5 | class TestPkiMixin(TestX509Mixin): 6 | ca_model = load_model("django_x509", "Ca") 7 | cert_model = load_model("django_x509", "Cert") 8 | 9 | def _create_ca(self, **kwargs): 10 | if "organization" not in kwargs: 11 | kwargs["organization"] = None 12 | return super()._create_ca(**kwargs) 13 | 14 | def _create_cert(self, **kwargs): 15 | if "organization" not in kwargs: 16 | kwargs["organization"] = None 17 | return super()._create_cert(**kwargs) 18 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0041_default_groups_organizationconfigsettings_permission.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-06-21 17:49 2 | 3 | from django.db import migrations 4 | 5 | from . import assign_organization_config_settings_permissions_to_groups 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("config", "0040_vpnclient_ip_setnull"), 11 | ] 12 | 13 | operations = [ 14 | migrations.RunPython( 15 | code=assign_organization_config_settings_permissions_to_groups, 16 | reverse_code=migrations.operations.special.RunPython.noop, 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/connection/migrations/0009_alter_deviceconnection_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-08-24 12:35 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | migrations.swappable_dependency(settings.CONFIG_DEVICE_MODEL), 10 | ("connection", "0008_remove_conflicting_deviceconnections"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterUniqueTogether( 15 | name="deviceconnection", 16 | unique_together={("device", "credentials")}, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/geo/channels/consumers.py: -------------------------------------------------------------------------------- 1 | import swapper 2 | from django_loci.channels.base import BaseLocationBroadcast 3 | 4 | Location = swapper.load_model("geo", "Location") 5 | 6 | 7 | class LocationBroadcast(BaseLocationBroadcast): 8 | model = Location 9 | 10 | def is_authorized(self, user, location): 11 | result = super().is_authorized(user, location) 12 | # non superusers must also be members of the org 13 | if ( 14 | result 15 | and not user.is_superuser 16 | and not user.is_manager(location.organization) 17 | ): 18 | return False 19 | return result 20 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0032_update_legacy_vpn_backend.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def update_legacy_vpn_backend(apps, schema_editor): 5 | Vpn = apps.get_model("config", "Vpn") 6 | Vpn.objects.filter(backend="django_netjsonconfig.vpn_backends.OpenVpn").update( 7 | backend="openwisp_controller.vpn_backends.OpenVpn" 8 | ) 9 | 10 | 11 | class Migration(migrations.Migration): 12 | dependencies = [("config", "0031_update_vpn_dh_param")] 13 | 14 | operations = [ 15 | migrations.RunPython( 16 | update_legacy_vpn_backend, reverse_code=migrations.RunPython.noop 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /openwisp_controller/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 3, 0, "alpha") 2 | __version__ = VERSION # alias 3 | 4 | 5 | def get_version(): 6 | version = f"{VERSION[0]}.{VERSION[1]}" 7 | if VERSION[2]: 8 | version = f"{version}.{VERSION[2]}" 9 | if VERSION[3:] == ("alpha", 0): 10 | version = f"{version} pre-alpha" 11 | if "post" in VERSION[3]: 12 | version = f"{version}.{VERSION[3]}" 13 | else: 14 | if VERSION[3] != "final": 15 | try: 16 | rev = VERSION[4] 17 | except IndexError: 18 | rev = 0 19 | version = f"{version}{VERSION[3][0:1]}{rev}" 20 | return version 21 | -------------------------------------------------------------------------------- /openwisp_controller/config/migrations/0024_update_context_data.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def forward(apps, schema_editor): 5 | """ 6 | Updates default value of context field 7 | """ 8 | if not schema_editor.connection.alias == "default": 9 | return 10 | Config = apps.get_model("config", "Config") 11 | 12 | for config in Config.objects.filter(context__isnull=True): 13 | config.context = {} 14 | config.save() 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [("config", "0023_update_context")] 19 | 20 | operations = [migrations.RunPython(forward, reverse_code=migrations.RunPython.noop)] 21 | -------------------------------------------------------------------------------- /openwisp_controller/connection/static/connection/js/credentials.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | django.jQuery(function ($) { 3 | var selector = $("#id_connector"), 4 | showFields = function () { 5 | var fields = $( 6 | "#credentials_form fieldset > .form-row:not(.field-connector):not(.field-params), .jsoneditor-wrapper", 7 | ), 8 | value = selector.val(); 9 | if (!value) { 10 | fields.hide(); 11 | } else { 12 | fields.show(); 13 | } 14 | }; 15 | selector.change(function () { 16 | showFields(); 17 | }); 18 | 19 | $("#id_params").on("jsonschema-schemaloaded", function () { 20 | showFields(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/openwisp2/sample_connection/migrations/0002_default_group_permissions.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from openwisp_controller.connection.migrations import ( 4 | assign_command_permissions_to_groups, 5 | assign_permissions_to_groups, 6 | ) 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [("sample_connection", "0001_initial")] 11 | 12 | operations = [ 13 | migrations.RunPython( 14 | assign_permissions_to_groups, reverse_code=migrations.RunPython.noop 15 | ), 16 | migrations.RunPython( 17 | assign_command_permissions_to_groups, reverse_code=migrations.RunPython.noop 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /openwisp_controller/connection/migrations/0006_name_unique_per_organization.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-02-11 22:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("connection", "0005_device_connection_failure_reason"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="credentials", 14 | name="name", 15 | field=models.CharField(db_index=True, max_length=64), 16 | ), 17 | migrations.AlterUniqueTogether( 18 | name="credentials", unique_together={("name", "organization")} 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tests/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | create_superuser () { 4 | local username="$1" 5 | local email="$2" 6 | local password="$3" 7 | cat <