From 35a2433cbca0846beb83a56d253a4d411be07185 Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Tue, 2 Jun 2026 12:42:33 +0530 Subject: [PATCH 1/8] [feature] Wired check_pending_upgrades and send_pending_upgrade_reminders into the Beat schedule #623 Added a firmware_schedule alongside the existing per-module schedules in images/common/openwisp/celery.py, gated on USE_OPENWISP_FIRMWARE and USE_OPENWISP_CELERY_FIRMWARE. Cadences read from two new env vars, OPENWISP_FIRMWARE_CHECK_PENDING_PERIOD_MINUTES (default 10) and OPENWISP_FIRMWARE_REMINDER_SCAN_PERIOD_DAYS (default 7), exposed in .env and the openwisp_base Dockerfile. Documented both env vars in docs/user/settings.rst. Extended the test_celery assertion in tests/runtests.py with the three new task names (check_pending_upgrades, retry_pending_upgrade, send_pending_upgrade_reminders). Related to #623 --- .env | 2 ++ docs/user/settings.rst | 19 +++++++++++++++++++ images/common/openwisp/celery.py | 18 ++++++++++++++++++ images/openwisp_base/Dockerfile | 2 ++ tests/runtests.py | 3 +++ 5 files changed, 44 insertions(+) diff --git a/.env b/.env index 1967becc..126cb9e8 100644 --- a/.env +++ b/.env @@ -61,6 +61,8 @@ OPENWISP_CELERY_MONITORING_COMMAND_FLAGS=--concurrency=1 OPENWISP_CELERY_MONITORING_CHECKS_COMMAND_FLAGS=--concurrency=1 USE_OPENWISP_CELERY_FIRMWARE=True OPENWISP_CELERY_FIRMWARE_COMMAND_FLAGS=--concurrency=1 +OPENWISP_FIRMWARE_CHECK_PENDING_PERIOD_MINUTES=10 +OPENWISP_FIRMWARE_REMINDER_SCAN_PERIOD_DAYS=7 CELERY_SERVICE_NETWORK_MODE=service:openvpn # Metric collection METRIC_COLLECTION=True diff --git a/docs/user/settings.rst b/docs/user/settings.rst index a2425c8c..835d8fa5 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -370,6 +370,25 @@ framework. - **Valid Values:** STRING - **Default:** ``--concurrency=1`` +``OPENWISP_FIRMWARE_CHECK_PENDING_PERIOD_MINUTES`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **Explanation:** Period in minutes between scans of pending persistent + firmware upgrades whose ``next_retry_at`` has elapsed. +- **Valid Values:** INTEGER. +- **Default:** ``10`` + +``OPENWISP_FIRMWARE_REMINDER_SCAN_PERIOD_DAYS`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **Explanation:** Period in days between scans for long-running + persistent batches that need a pending-upgrade reminder + notification. The per-batch reminder cadence itself is controlled + upstream by ``OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_REMINDER_PERIOD`` + (default 60 days). +- **Valid Values:** INTEGER. +- **Default:** ``7`` + ``USE_OPENWISP_CELERY_MONITORING`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/images/common/openwisp/celery.py b/images/common/openwisp/celery.py index 72dd0f97..31b6c39d 100644 --- a/images/common/openwisp/celery.py +++ b/images/common/openwisp/celery.py @@ -13,6 +13,7 @@ {}, {}, ) +firmware_schedule = {} task_routes = {} if env_bool(os.environ.get("USE_OPENWISP_CELERY_NETWORK")): @@ -40,6 +41,22 @@ task_routes["openwisp_firmware_upgrader.tasks.batch_upgrade_operation"] = { "queue": "firmware_upgrader" } + check_pending_minutes = int( + os.environ.get("OPENWISP_FIRMWARE_CHECK_PENDING_PERIOD_MINUTES", "10") + ) + reminder_scan_days = int( + os.environ.get("OPENWISP_FIRMWARE_REMINDER_SCAN_PERIOD_DAYS", "7") + ) + firmware_schedule = { + "check_pending_upgrades": { + "task": "openwisp_firmware_upgrader.tasks.check_pending_upgrades", + "schedule": timedelta(minutes=check_pending_minutes), + }, + "send_pending_upgrade_reminders": { + "task": "openwisp_firmware_upgrader.tasks.send_pending_upgrade_reminders", + "schedule": timedelta(days=reminder_scan_days), + }, + } if env_bool(os.environ.get("USE_OPENWISP_RADIUS")): radius_schedule = { @@ -93,6 +110,7 @@ **notification_schedule, **monitoring_schedule, **metric_collection_schedule, + **firmware_schedule, }, ) app.config_from_object("django.conf:settings", namespace="CELERY") diff --git a/images/openwisp_base/Dockerfile b/images/openwisp_base/Dockerfile index 30e26c55..b1364dcc 100644 --- a/images/openwisp_base/Dockerfile +++ b/images/openwisp_base/Dockerfile @@ -175,4 +175,6 @@ ENV DASHBOARD_APP_SERVICE=dashboard \ CRON_DELETE_OLD_POSTAUTH=365 \ CRON_CLEANUP_STALE_RADACCT=365 \ CRON_DELETE_OLD_RADIUSBATCH_USERS=365 \ + OPENWISP_FIRMWARE_CHECK_PENDING_PERIOD_MINUTES=10 \ + OPENWISP_FIRMWARE_REMINDER_SCAN_PERIOD_DAYS=7 \ METRIC_COLLECTION=True diff --git a/tests/runtests.py b/tests/runtests.py index a13a3766..5ef115a6 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -402,8 +402,11 @@ def test_celery(self): "openwisp_controller.subnet_division.tasks.update_subnet_division_index", "openwisp_controller.subnet_division.tasks.update_subnet_name_description", "openwisp_firmware_upgrader.tasks.batch_upgrade_operation", + "openwisp_firmware_upgrader.tasks.check_pending_upgrades", "openwisp_firmware_upgrader.tasks.create_all_device_firmwares", "openwisp_firmware_upgrader.tasks.create_device_firmware", + "openwisp_firmware_upgrader.tasks.retry_pending_upgrade", + "openwisp_firmware_upgrader.tasks.send_pending_upgrade_reminders", "openwisp_firmware_upgrader.tasks.upgrade_firmware", "openwisp_monitoring.check.tasks.auto_create_check", "openwisp_monitoring.check.tasks.perform_check", From 54fb1e93bb13f7d8b1cbbd02228e7758496c00a3 Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Tue, 2 Jun 2026 13:26:34 +0530 Subject: [PATCH 2/8] =?UTF-8?q?[chores]=20TEMPORARY=20pin=20openwisp-firmw?= =?UTF-8?q?are-upgrader=20to=20PR=20#436=20branch=20=E2=80=94=20revert=20b?= =?UTF-8?q?efore=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on this integration branch installs openwisp-firmware-upgrader~=1.2.0 from PyPI, which does not yet ship the three new tasks. Without this pin the new Beat entries and the test_celery assertion both fail. Points at the openwisp-firmware-upgrader branch issues/417-persistence-schema-fields (the PR #436 head), via the tarball URL so pip does not need git in the slim base image. Revert this to openwisp-firmware-upgrader~=1.2.0 (or the next released version) as the final commit before this branch merges to master, once PR #436 has merged and a release is cut. Related to #623 --- images/openwisp_base/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/openwisp_base/Dockerfile b/images/openwisp_base/Dockerfile index b1364dcc..18bb5242 100644 --- a/images/openwisp_base/Dockerfile +++ b/images/openwisp_base/Dockerfile @@ -33,7 +33,7 @@ RUN pip install --no-cache-dir --user --upgrade pip setuptools wheel ARG OPENWISP_MONITORING_SOURCE="openwisp-monitoring~=1.2.0" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_MONITORING_SOURCE} -ARG OPENWISP_FIRMWARE_SOURCE="openwisp-firmware-upgrader~=1.2.0" +ARG OPENWISP_FIRMWARE_SOURCE="https://github.com/openwisp/openwisp-firmware-upgrader/tarball/issues/417-persistence-schema-fields" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_FIRMWARE_SOURCE} ARG OPENWISP_TOPOLOGY_SOURCE="openwisp-network-topology~=1.2.0" From a95eadf448e14cdcf1c0c49b08bbb6595f8a05dd Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Thu, 4 Jun 2026 17:23:50 +0530 Subject: [PATCH 3/8] =?UTF-8?q?[chores]=20TEMPORARILY=20pin=20openwisp=201?= =?UTF-8?q?.3=20family=20for=20PR=20#436=20compat=20=E2=80=94=20revert=20b?= =?UTF-8?q?efore=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After rebasing PR #436 (openwisp-firmware-upgrader) onto its master, the firmware-upgrader code now imports openwisp_utils.api.pagination, which only exists in the openwisp 1.3 family. The existing firmware-upgrader tarball pin (54fb1e9) is no longer enough on its own: openwisp-monitoring/topology/radius 1.2 from PyPI pull in openwisp-utils 1.2 transitively, which lacks api.pagination, and CI fails with ModuleNotFoundError during dashboard data.setup(). Switch the conditional --force-reinstall ARGs (utils, users, controller, notifications, ipam) from default to the matching 1.3 branch tarballs so the entire family is consistent. monitoring, topology and radius stay on 1.2 — only the firmware-upgrader's direct dependencies need bumping. Revert these alongside 54fb1e9 once PR #436 has merged and an openwisp-firmware-upgrader release (and its 1.3 family deps) are cut. Related to #436 Related to #623 --- images/openwisp_base/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/images/openwisp_base/Dockerfile b/images/openwisp_base/Dockerfile index 18bb5242..97ba47a5 100644 --- a/images/openwisp_base/Dockerfile +++ b/images/openwisp_base/Dockerfile @@ -46,27 +46,27 @@ RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_RADIUS_SOURCE} # here we try to install custom versions of the modules only if the # supplied argument does not equal the default value, because # otherwise these modules will have already been installed above -ARG OPENWISP_IPAM_SOURCE=default +ARG OPENWISP_IPAM_SOURCE="https://github.com/openwisp/openwisp-ipam/archive/refs/heads/1.3.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_IPAM_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade ${OPENWISP_IPAM_SOURCE}; \ fi -ARG OPENWISP_CONTROLLER_SOURCE=default +ARG OPENWISP_CONTROLLER_SOURCE="https://github.com/openwisp/openwisp-controller/archive/refs/heads/1.3.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_CONTROLLER_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade ${OPENWISP_CONTROLLER_SOURCE}; \ fi -ARG OPENWISP_NOTIFICATION_SOURCE=default +ARG OPENWISP_NOTIFICATION_SOURCE="https://github.com/openwisp/openwisp-notifications/archive/refs/heads/1.3.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_NOTIFICATION_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade ${OPENWISP_NOTIFICATION_SOURCE}; \ fi -ARG OPENWISP_USERS_SOURCE=default +ARG OPENWISP_USERS_SOURCE="https://github.com/openwisp/openwisp-users/archive/refs/heads/1.3.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_USERS_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade --force-reinstall ${OPENWISP_USERS_SOURCE}; \ fi -ARG OPENWISP_UTILS_SOURCE=default +ARG OPENWISP_UTILS_SOURCE="https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_UTILS_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade --force-reinstall "${OPENWISP_UTILS_SOURCE}"; \ From 90afffab2b36110a29fbd892bdab3b4415767113 Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Thu, 4 Jun 2026 17:23:57 +0530 Subject: [PATCH 4/8] [fix] Reformat docs/user/settings.rst #623 The two settings entries added in 35a2433 (the explanation paragraph for OPENWISP_FIRMWARE_REMINDER_SCAN_PERIOD_DAYS) wrap to a different width than docstrfmt --line-length 74 expects, which is what openwisp-qa-check runs in CI. Re-wrapped to match. Related to #623 --- docs/user/settings.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user/settings.rst b/docs/user/settings.rst index 835d8fa5..f795b906 100644 --- a/docs/user/settings.rst +++ b/docs/user/settings.rst @@ -382,10 +382,10 @@ framework. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - **Explanation:** Period in days between scans for long-running - persistent batches that need a pending-upgrade reminder - notification. The per-batch reminder cadence itself is controlled - upstream by ``OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_REMINDER_PERIOD`` - (default 60 days). + persistent batches that need a pending-upgrade reminder notification. + The per-batch reminder cadence itself is controlled upstream by + ``OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_REMINDER_PERIOD`` (default 60 + days). - **Valid Values:** INTEGER. - **Default:** ``7`` From b1880cd3386c0747f720d96c5b21fe832d8eb9d3 Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Wed, 10 Jun 2026 23:19:44 +0530 Subject: [PATCH 5/8] [fix] Pin topology/monitoring/radius to 1.3 to match utils, fixing CI #623 a95eadf bumped utils and controller to the 1.3 family but left OPENWISP_TOPOLOGY/MONITORING/RADIUS_SOURCE on ~=1.2.0. network-topology 1.2's admin.py imports UUIDAdmin, which openwisp-utils 1.3 removed, so daphne crashed on import during admin autodiscover and CI failed. CI reads the Dockerfile defaults (.build.env is gitignored), so the pin must live here. Pin the whole openwisp 1.3 family to fixed commit SHAs so the build is consistent and reproducible. Temporary for the firmware-upgrader PR compatibility; revert with the rest of the 1.3 pins before merge. Related to #623 --- images/openwisp_base/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/images/openwisp_base/Dockerfile b/images/openwisp_base/Dockerfile index 97ba47a5..14f3e061 100644 --- a/images/openwisp_base/Dockerfile +++ b/images/openwisp_base/Dockerfile @@ -30,16 +30,16 @@ ENV PATH="${PATH}:/home/openwisp/.local/bin" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade pip setuptools wheel -ARG OPENWISP_MONITORING_SOURCE="openwisp-monitoring~=1.2.0" +ARG OPENWISP_MONITORING_SOURCE="https://github.com/openwisp/openwisp-monitoring/archive/584b29db6b8ee2657cdb14722c3b129afe9a417f.tar.gz" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_MONITORING_SOURCE} ARG OPENWISP_FIRMWARE_SOURCE="https://github.com/openwisp/openwisp-firmware-upgrader/tarball/issues/417-persistence-schema-fields" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_FIRMWARE_SOURCE} -ARG OPENWISP_TOPOLOGY_SOURCE="openwisp-network-topology~=1.2.0" +ARG OPENWISP_TOPOLOGY_SOURCE="https://github.com/openwisp/openwisp-network-topology/archive/2f23a6c8a16af19643f1abba67d436da349be3e6.tar.gz" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_TOPOLOGY_SOURCE} -ARG OPENWISP_RADIUS_SOURCE="openwisp-radius~=1.2.0" +ARG OPENWISP_RADIUS_SOURCE="https://github.com/openwisp/openwisp-radius/archive/cd6a22b1d27a1b99b7155a120556e370fea98bfd.tar.gz" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_RADIUS_SOURCE} @@ -51,7 +51,7 @@ ARG OPENWISP_IPAM_SOURCE="https://github.com/openwisp/openwisp-ipam/archive/refs RUN if [ "$OPENWISP_IPAM_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade ${OPENWISP_IPAM_SOURCE}; \ fi -ARG OPENWISP_CONTROLLER_SOURCE="https://github.com/openwisp/openwisp-controller/archive/refs/heads/1.3.tar.gz" +ARG OPENWISP_CONTROLLER_SOURCE="https://github.com/openwisp/openwisp-controller/archive/e0ee749e22c7fa1445d8ce9ad1d289c08ce74feb.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_CONTROLLER_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade ${OPENWISP_CONTROLLER_SOURCE}; \ @@ -66,7 +66,7 @@ ARG OPENWISP_USERS_SOURCE="https://github.com/openwisp/openwisp-users/archive/re RUN if [ "$OPENWISP_USERS_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade --force-reinstall ${OPENWISP_USERS_SOURCE}; \ fi -ARG OPENWISP_UTILS_SOURCE="https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz" +ARG OPENWISP_UTILS_SOURCE="https://github.com/openwisp/openwisp-utils/archive/764ea4917c2b52cddbf2e45ff9c67ac6e4c87261.tar.gz" # hadolint ignore=DL3013 RUN if [ "$OPENWISP_UTILS_SOURCE" != "default" ] ; then \ pip install --no-cache-dir --user --upgrade --force-reinstall "${OPENWISP_UTILS_SOURCE}"; \ From 66a33dc05436eba67a30c8e45323323e79a5991b Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Thu, 11 Jun 2026 00:29:20 +0530 Subject: [PATCH 6/8] [fix] Make create_ssh_key_template idempotent by guarding on name #623 create_ssh_key_template checked for an existing template via config__contains, which does not match the stored JSON config, so the guard never short-circuited and a second bootstrap run tried to recreate the shared "SSH Keys" template. openwisp-controller 1.3 enforces unique shared-template names, so this raised ValidationError, aborted the bootstrap and left uwsgi down (nginx 502 in the Test step). Guard on the template name like the sibling create_* helpers already do. Related to #623 --- images/openwisp_dashboard/load_init_data.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/images/openwisp_dashboard/load_init_data.py b/images/openwisp_dashboard/load_init_data.py index 66239643..4b70654e 100644 --- a/images/openwisp_dashboard/load_init_data.py +++ b/images/openwisp_dashboard/load_init_data.py @@ -167,12 +167,8 @@ def create_default_credentials(): def create_ssh_key_template(): - if Template.objects.filter( - default=True, config__contains="/etc/dropbear/authorized_keys" - ).exists(): - return Template.objects.filter( - default=True, config__contains="/etc/dropbear/authorized_keys" - ).first() + if Template.objects.filter(name="SSH Keys").exists(): + return Template.objects.filter(name="SSH Keys").first() public_key_filepath = os.environ["SSH_PUBLIC_KEY_PATH"] try: with open(public_key_filepath, "r") as file: From 7d9651ce0c74eba251f751249a24f6f034cff48a Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Thu, 11 Jun 2026 00:59:07 +0530 Subject: [PATCH 7/8] [fix] Register firmware-upgrader API URLs in the dashboard urlconf #623 The device-firmware admin inline reverses the "upgrader" namespace for the upgrade-operation cancel button, but the dashboard urlconf only wired the firmware private-storage URLs, not the API URLs that define that namespace. Rendering the device admin page raised NoReverseMatch and returned 500 (nginx then reported 502), failing the Test step. Include the firmware-upgrader API URLs so the namespace is reversible from the dashboard; nginx still routes the actual requests to the API container. Related to #623 --- images/openwisp_dashboard/urls.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/images/openwisp_dashboard/urls.py b/images/openwisp_dashboard/urls.py index 3f27d473..76e3b7c7 100644 --- a/images/openwisp_dashboard/urls.py +++ b/images/openwisp_dashboard/urls.py @@ -42,6 +42,12 @@ include((fw_private_storage_urls, "firmware"), namespace="firmware"), ) ] + # register the REST API URLs so admin templates can reverse the + # "upgrader" namespace (e.g. the upgrade-operation cancel button); + # nginx routes the actual requests to the API container. + urlpatterns += [ + path("api/v1/", include("openwisp_firmware_upgrader.api.urls")), + ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += staticfiles_urlpatterns() From 1436b4c4805e9c50bb1933f4bc6925e80c10ee3a Mon Sep 17 00:00:00 2001 From: Eeshu-Yadav Date: Thu, 11 Jun 2026 01:22:24 +0530 Subject: [PATCH 8/8] [fix] Keep openwisp-radius on 1.2 so its tasks match the CI test #623 I had pinned radius to a 1.3 commit with the rest of the family, but openwisp-radius 1.3 removed the deactivate_expired_users task that docker-openwisp's test_celery expects, failing the Test step. radius 1.2 still ships that task and does not import the openwisp-utils admin symbols that 1.3 removed, so it stays compatible with the pinned utils 1.3. Revert radius to the released 1.2 (the family-pin commit intentionally kept radius on 1.2; only utils/controller/topology/monitoring need the 1.3 line). Related to #623 --- images/openwisp_base/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/openwisp_base/Dockerfile b/images/openwisp_base/Dockerfile index 14f3e061..ff398cfc 100644 --- a/images/openwisp_base/Dockerfile +++ b/images/openwisp_base/Dockerfile @@ -39,7 +39,7 @@ RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_FIRMWARE_SOURCE} ARG OPENWISP_TOPOLOGY_SOURCE="https://github.com/openwisp/openwisp-network-topology/archive/2f23a6c8a16af19643f1abba67d436da349be3e6.tar.gz" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_TOPOLOGY_SOURCE} -ARG OPENWISP_RADIUS_SOURCE="https://github.com/openwisp/openwisp-radius/archive/cd6a22b1d27a1b99b7155a120556e370fea98bfd.tar.gz" +ARG OPENWISP_RADIUS_SOURCE="openwisp-radius~=1.2.0" # hadolint ignore=DL3013 RUN pip install --no-cache-dir --user --upgrade ${OPENWISP_RADIUS_SOURCE}