Skip to content

[feature] Persistent Mass Upgrades#436

Open
Eeshu-Yadav wants to merge 47 commits into
gsoc26-persistent-scheduled-upgradesfrom
issues/417-persistence-schema-fields
Open

[feature] Persistent Mass Upgrades#436
Eeshu-Yadav wants to merge 47 commits into
gsoc26-persistent-scheduled-upgradesfrom
issues/417-persistence-schema-fields

Conversation

@Eeshu-Yadav

@Eeshu-Yadav Eeshu-Yadav commented May 22, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #417
Closes #422
Closes #423
Closes #424
Closes #425
Closes #426
Closes #427
Closes #428
Closes #429
Closes #430

Related to #379 (parent - Persistent Mass Upgrades, Phase 1).

Description of Changes

Today a mass upgrade that hits an offline device goes from in-progress to failed once Celery's auto-retries run out (about 10 minutes), and the admin has to retry each failed device by hand. That is manageable for one device but not for hundreds.

This PR adds a persistent mode. An offline device now moves to a new pending status with a backoff-scheduled retry time instead of failing; a periodic Beat task picks up due operations and re-dispatches the upgrade, and the batch stays active until every device is upgraded or cancelled. The four sub-issues land together because the new status is only coherent once its schema (#417), the failure handler that produces it (#422), the batch/conflict/cancel code that has to recognise it (#423), and the Beat scanner that retries it (#424) are all present. Shipping any one alone either leaves the status unused or breaks the existing failure path.

Changes in this PR:

  • Offline devices move to pending with a scheduled next-retry time. A Beat task runs every 10 minutes (configurable) and re-dispatches due retries using exponential backoff (10m, 20m, 40m, ... capped at 12h, with ±25% jitter) until the device returns or the admin cancels.
  • Pending operations can be cancelled from the admin and the REST API, the same as in-progress ones.
  • The concurrent-upgrade guard treats pending as a blocking state, so a second upgrade targeting the same device will not double-flash it.
  • A device deactivated while pending stops retrying and its operation is marked failed.
  • The batch page shows "X complete, Y pending" on first load and on every WebSocket update, both driven by the same count so the display does not flicker when the socket connects.
  • is_persistent is locked once the operation leaves idle, so the policy cannot be changed mid-run.
  • The device-firmware REST endpoint accepts is_persistent, letting single-device upgrades opt into persistence (off by default).
  • Admins are notified when a batch still has pending devices after a configurable period.
  • With openwisp-monitoring installed, a device coming back online retries its pending operations immediately instead of waiting for the next Beat scan.

Screenshots

Screenshot from 2026-07-01 20-26-07 Screenshot from 2026-06-03 17-19-42 Screenshot from 2026-06-03 16-52-52 Screenshot from 2026-06-03 16-52-52

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: df851e20-df6c-48e2-969a-d6499ab3d265

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds persistent firmware upgrade support. Batch and standalone operations gain persistence and retry metadata, while pending becomes a first-class status with backoff scheduling, Beat retries, monitoring wake-ups, cancellation, and notifications. Batch aggregation, admin/API interfaces, WebSocket payloads, frontend rendering, migrations, tests, settings, and documentation are updated accordingly.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: nemesifier, asmodehn


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
General Rules ❌ Error The no-connection branch still leaves non-persistent upgrades in-progress (see test_no_connection_non_persistent_op_stays_in_progress), so unreachable devices can block later upgrades. Change the non-persistent NoWorkingDeviceConnectionError(connection=None) path to a terminal failure state and add a regression test; also format next_retry_at with timezone.localtime(...).
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title is descriptive and clearly matches the persistent mass-upgrades feature.
Description check ✅ Passed The description matches the template with checklist items, issue references, a change summary, and screenshots.
Linked Issues check ✅ Passed The implementation covers the linked model, retry, batch, monitoring, notification, admin, API, testing, and docs changes.
Out of Scope Changes check ✅ Passed I don't see unrelated changes; the diff stays focused on persistent upgrades, supporting docs, tests, and deployment wiring.
Bug Fixes ✅ Passed PASS: the persistent-offline path now routes to pending/retry, and regression tests cover the bug flow, UI cancel/pending behavior, and async timing via mocks/time_travel.
Features ✅ Passed PASS: Linked issue #417 exists on the GSoC26 board (In progress), docs add a new persistent-mass-upgrades page, tests include new Selenium/browser coverage, and the PR description includes screensh...
Changes ✅ Passed Docs were updated for the new pending/persistent behavior, the PR description includes UI screenshots, tests cover the new flow, and API changes are additive with defaults.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/417-persistence-schema-fields

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from ace6c8c to 5119b30 Compare May 23, 2026 10:57
@Eeshu-Yadav
Eeshu-Yadav marked this pull request as ready for review May 23, 2026 11:21
Copilot AI review requested due to automatic review settings May 23, 2026 11:21
@Eeshu-Yadav

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kilo-code-bot

kilo-code-bot Bot commented May 23, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 file)
  • openwisp_firmware_upgrader/tests/test_tasks.py
Previous Review Summaries (10 snapshots, latest commit 74ff536)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 74ff536)

Status: No Issues Found | Recommendation: Merge

Incremental re-review at HEAD 74ff536 (previous review b49103e). The changes since the last review are limited to help_text string rewording on the persistence model fields (is_persistent, retry_count, next_retry_at) and the matching edits in migrations 0018_* and sample 0005_*. These are cosmetic documentation edits with no logic, schema, or behavior change.

Files Reviewed (incremental, 3 files)
  • openwisp_firmware_upgrader/base/models.py - help_text rewording, no issues
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - help_text sync, no issues
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py - help_text sync, no issues

Previous review (commit b49103e)

Status: No Issues Found | Recommendation: Merge

Incremental re-review at HEAD b49103e (previous review 49c90c2b). The changes since the last review are a focused, correct bug fix for how a non-persistent upgrade operation behaves when no usable device connection exists.

  • base/models.py changes the non-persistent branch so that an operation with error.connection is None now sets status = "aborted" and saves, instead of leaving the operation stuck in in-progress forever. The log line correctly uses save=False so it is persisted atomically with the status update, matching the persistent branch.
  • tests/test_models.py renames test_no_connection_non_persistent_op_stays_in_progress to test_no_connection_non_persistent_op_aborts and updates the assertion to expect "aborted", properly covering the new behavior.

No new critical bugs or security vulnerabilities were introduced. The change resolves a previously latent stuck-state bug and is backed by a regression test.

Files Reviewed (incremental, 2 files)
  • openwisp_firmware_upgrader/base/models.py - non-persistent no-connection abort fix, no issues
  • openwisp_firmware_upgrader/tests/test_models.py - updated regression test, no issues

Previous review (commit 49c90c2)

Status: No Issues Found | Recommendation: Merge

Incremental re-review at HEAD 49c90c2b (previous review 87d12063). The changes since the last review are a focused, safe improvement to how the upgrade_firmware task handles an operation that was cancelled or deleted after dispatch.

  • tasks.py removes the early status="in-progress" DB guard and instead re-reads the row via refresh_from_db() before calling upgrade(). The upgrade() method already early-returns when status is not in-progress, so a cancellation issued after dispatch is now correctly honored. A new except ObjectDoesNotExist: return cleanly handles the row being deleted between fetch and refresh.
  • test_tasks.py updates test_upgrade_firmware_skips_when_not_in_progress and adds two regression tests covering cancellation and deletion during the dispatch window. Both assert the upgrade path is not reached.
  • base/models.py switches the retry-time log to timezone.localtime(...), a correct fix for timezone-aware display.
  • CSS change is cosmetic.

No new critical bugs or security issues were introduced. The previously flagged 0018 migration blocking-index warning is outside this incremental diff and unchanged.

Files Reviewed (incremental, 4 files)
  • openwisp_firmware_upgrader/base/models.py - timezone-aware retry log, no issues
  • openwisp_firmware_upgrader/tasks.py - re-read-before-upgrade cancellation handling, no issues
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css - cosmetic padding cleanup, no issues
  • openwisp_firmware_upgrader/tests/test_tasks.py - updated + new cancellation/dispatch regression tests, no issues

Previous review (commit 87d1206)

Status: 1 Issue Found | Recommendation: Merge with follow-up

Incremental re-review at HEAD 87d12063 (previous review a56e5aa). The changes since then are safe and minor: removed the premature 1.3.0 [unreleased] changelog entry, reordered log among the admin readonly fields, refined a docstring in apps.py, improved persistent-retry log formatting (split into two log_line calls with newlines and a human-readable next_retry_at timestamp) in base/models.py and tasks.py, and strengthened the monitoring-signal test plus added a new test asserting log_line emits firmware_upgrader_log_updated with the expected line kwarg. No new bug or security issue was introduced, and the new/updated tests align with the emitted signal contract.

The only carried-forward finding is on the unchanged 0018 migration (not part of this incremental diff): next_retry_at still adds a blocking non-concurrent index. It already has an active inline comment, so no duplicate was posted.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 52 next_retry_at adds db_index=True, producing a blocking non-concurrent CREATE INDEX on the populated upgradeoperation table during deploy. Consider a maintenance window or CREATE INDEX CONCURRENTLY. Already flagged inline (unchanged in this incremental diff).
Files Reviewed (incremental)
  • CHANGES.rst - removed premature unreleased entry, no issues
  • openwisp_firmware_upgrader/admin.py - readonly field reorder, no issues
  • openwisp_firmware_upgrader/apps.py - docstring wording, no issues
  • openwisp_firmware_upgrader/base/models.py - persistent-retry log formatting, no issues
  • openwisp_firmware_upgrader/tasks.py - retry log newline, no issues
  • openwisp_firmware_upgrader/tests/test_handlers.py - updated + new log-signal test, no issues

Fix these issues in Kilo Cloud

Previous review (commit a56e5aa)

Status: 1 Issue Found | Recommendation: Merge with follow-up

Incremental re-review at HEAD a56e5aa (previous review 76ac8f0). The changes since then are safe: docs consolidation for persistent mass upgrades, resolution of the deploy-time index-lock concerns, and a switch from two custom notification types (pending_upgrade_reminder, persistent_upgrade_failed) to the built-in generic_message type.

The notification refactor is correct: both call sites now pass the required message kwarg for generic_message, the reminder adds a url linking to the batch filtered to pending devices (?status=pending), the custom register_notification_types registration was removed from apps.py, and tests were updated to match (including removal of the now-obsolete registration tests). No new bug or security issue was introduced.

One of the two previously flagged index locks is now resolved: 0019 (and the sample migration 0006) dropped db_index=True on last_reminder_at, and the model field lost its index too. The remaining next_retry_at non-concurrent index in 0018 is documented via an explanatory comment but still builds a blocking CREATE INDEX on deploy; it is carried forward below and already has an active inline comment (no duplicate posted).

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 52 next_retry_at adds db_index=True, producing a blocking non-concurrent CREATE INDEX on the populated upgradeoperation table during deploy. Now documented via an inline comment; consider a maintenance window or CREATE INDEX CONCURRENTLY. Already flagged inline.
Resolved Since Last Review
  • 0019_batchupgradeoperation_last_reminder_at.py / sample 0006 - blocking db_index=True on last_reminder_at removed.
Files Reviewed (incremental)
  • openwisp_firmware_upgrader/apps.py - custom notification-type registration removed, no issues
  • openwisp_firmware_upgrader/base/models.py - switched to generic_message, no issues
  • openwisp_firmware_upgrader/tasks.py - reminder now uses generic_message + admin url, no issues
  • openwisp_firmware_upgrader/tests/test_notifications.py - tests updated, no issues
  • openwisp_firmware_upgrader/migrations/0018* - next_retry_at index carried forward (WARNING)
  • openwisp_firmware_upgrader/migrations/0019* - index lock resolved
  • docs/* - documentation consolidation, no issues
  • requirements-test.txt - pinned openwisp-notifications, no issues

Fix these issues in Kilo Cloud

Previous review (commit 76ac8f0)

Status: 2 Issues Found | Recommendation: Merge with follow-up

Incremental re-review at HEAD 76ac8f0. The only change since the previous review (99b5988) is a one-line explanatory comment in check_pending_upgrades (openwisp_firmware_upgrader/tasks.py:172) documenting why retries are spread with a random countdown. It is accurate and introduces no bug or security issue, so no new inline comments were posted.

The two previously flagged deploy-time migration index locks remain unresolved on unchanged migration files and are carried forward below (both already have active inline comments). The core persistent mass-upgrade pipeline is unchanged and remains sound: atomic claim on retry, pending treated as in-progress for batch aggregation, immutable is_persistent after launch, cancel covering ('in-progress','pending'), deactivated-device abort, and an idempotent reminder task.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True, producing a blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already flagged inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True, producing a blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy (already flagged inline). Consider a maintenance window or RunSQL CREATE INDEX CONCURRENTLY.
Files Reviewed (incremental)
  • openwisp_firmware_upgrader/tasks.py - comment-only change, no issues

Fix these issues in Kilo Cloud

Previous review (commit 99b5988)

Status: 2 Issues Found | Recommendation: Merge with follow-up

Re-reviewed the persistent mass-upgrade pipeline at the current HEAD. The core logic remains solid: retry_pending_upgrade uses an atomic UPDATE ... WHERE status='pending' claim to prevent double-flash, check_pending_upgrades re-claims each op before dispatch, deactivated devices abort the retry chain safely, batch status aggregation correctly treats pending as in-progress (no premature success/failed), is_persistent is immutable after launch on both models, cancel covers ('in-progress', 'pending'), and the reminder task is idempotent via a claimed last_reminder_at update. Notification types are registered and monitoring signals are wired defensively behind an optional-dependency check. The JS layer escapes all server-derived data (escapeHtml) and guards missing pending/total. Tests cover models, tasks, notifications, handlers, admin, API, and selenium; docs updated.

No new critical bugs or security issues found in the changed code. The only outstanding items are two deploy-time migration index locks, both already flagged inline.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True, producing a blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already flagged inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True, producing a blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy (already flagged inline). Consider a maintenance window or RunSQL CREATE INDEX CONCURRENTLY.
Files Reviewed
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py - 1 issue
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • openwisp_firmware_upgrader/tests/* (models, tasks, notifications, handlers, admin, api, websockets, selenium)
  • docs/user/persistent-mass-upgrades.rst, docs/user/rest-api.rst, docs/user/settings.rst, docs/user/upgrade-status.rst, docs/user/websocket-api.rst, docs/index.rst, docs/user/intro.rst

Fix these issues in Kilo Cloud

Previous review (commit 8c60dc3)

Status: 2 Issues Found | Recommendation: Merge with follow-up

The persistent mass-upgrade pipeline is solid: atomic status claims (retry_pending_upgrade mutex prevents double-flash), correct pending handling in batch status aggregation (no premature success/failed), tenant-scoped cancel/delete via ProtectedAPIMixin, immutable is_persistent enforced on creation paths, and broad test coverage (handlers, tasks, notifications, selenium). The websockets/JS layer escapes all server-derived data and guards against missing pending/NaN. No new critical bugs or security issues found in the changed code.

The only outstanding items are two deploy-time migration index locks, both already noted inline. Consider a maintenance window or a RunSQL CREATE INDEX CONCURRENTLY for these.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already commented inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy (already commented inline). Consider a maintenance window or a concurrent index via RunSQL.
Files Reviewed
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py - 1 issue
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst

Fix these issues in Kilo Cloud

Previous review (commit b6f5543)

Status: 2 Issues Found | Recommendation: Merge with follow-up

The persistent mass-upgrade pipeline is solid: atomic status claims, conditional re-checks before dispatch, an immutable is_persistent flag, tenant-scoped cancel/delete guards, and broad test coverage (handlers, tasks, notifications, selenium). The only outstanding items are two deploy-time migration index locks, both already noted inline. The previously reported REST API documentation gap is now resolved — is_persistent/pending are documented in rest-api.rst.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already commented inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy (already commented inline). Consider a maintenance window or a concurrent index via RunSQL.
Files Reviewed
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/websockets.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py - 1 issue
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py - 1 issue
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst

Fix these issues in Kilo Cloud

Previous review (commit a6f27f0)

Status: 3 Issues Found | Recommendation: Merge with follow-up

The persistent mass-upgrade pipeline is solid: atomic status claims, conditional failed/in-progress re-checks before dispatch, an immutable is_persistent, tenant-scoped cancel/delete guards, and broad test coverage (handlers, tasks, notifications, selenium). No critical bugs or security issues were found in the changed code. Remaining items are deploy-time migration notes and a documentation gap.

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py 45 next_retry_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated upgradeoperation table during deploy (already commented inline).
openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py 18 last_reminder_at adds db_index=True → blocking CREATE INDEX (no CONCURRENTLY) on the populated batchupgradeoperation table during deploy. Same class as 0018:45; consider a maintenance window or a concurrent index via RunSQL (newly commented inline).
docs/user/rest-api.rst 215 REST API reference not updated: POST /build/{id}/upgrade/ omits the new is_persistent/upgrade_all body params, and the operation status lists omit pending (file is outside the diff, so noted here only).
Other Observations (not in diff)

The operation_update payload doc in docs/user/websocket-api.rst still omits is_persistent, retry_count and next_retry_at, which UpgradeOperationSerializer now pushes over the device channel.

The Beat scanner (tasks.py) dispatches one retry_pending_upgrade task per due pending operation with no cap/batching; for a very large offline fleet this can fan out thousands of broker publishes per tick — a follow-up to stream/batch the dispatch would be welcome, but it is not a correctness bug.

Files Reviewed (18 files)
  • `openwisp_f

[Snapshot truncated.]

Additional previous summary content was truncated to keep this comment within platform limits.


Reviewed by step-3.7-flash · Input: 183.3K · Output: 10K · Cached: 1.4M

@coderabbitai coderabbitai Bot added the enhancement New feature or request label May 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR lays the model-layer groundwork for “persistent” mass upgrades by extending the upgrade/batch operation schema, adding a new pending operation status, propagating batch persistence to child operations, and enforcing immutability of the persistence flag after launch.

Changes:

  • Added persistent flags to batch and per-device upgrade operations, plus retry_count and next_retry_at fields for retry scheduling.
  • Introduced pending to UpgradeOperation.STATUS_CHOICES and propagated batch.persistent into child operations during creation.
  • Added unit tests for defaults, propagation, DB index presence, and immutability guards; added corresponding migrations (including sample app migration).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
openwisp_firmware_upgrader/base/models.py Adds persistence fields, pending status, batch→child propagation, and immutability validation in clean().
openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py Introduces DB fields and extends status choices at the schema level.
openwisp_firmware_upgrader/tests/test_models.py Adds tests for new field defaults, propagation behavior, DB index flag, and immutability validation.
tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py Mirrors the migration in the sample app used by the test project.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 843 to 847
("failed", _("failed")), # failed at late stage or can't reconnect
("cancelled", _("cancelled")), # cancelled by the user
("aborted", _("aborted")), # aborted due to prerequisites not met
("pending", _("pending")), # offline device; waiting for periodic retry
)
("failed", _("failed")), # failed at late stage or can't reconnect
("cancelled", _("cancelled")), # cancelled by the user
("aborted", _("aborted")), # aborted due to prerequisites not met
("pending", _("pending")), # offline device; waiting for periodic retry
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/base/models.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 617-631: When checking immutability for the persistent field in
the model (inside the BatchUpgradeOperation instance logic), use the persisted
DB status instead of the in-memory self.status so callers can't flip status to
"idle" during the same edit; fetch the stored status via
load_model("BatchUpgradeOperation").objects.values_list("status",
flat=True).get(pk=self.pk) (similar to the existing persistent fetch) and change
the gate from self.status != "idle" to stored_status != "idle" before comparing
self.persistent to the persisted persistent and raising the ValidationError.

In `@openwisp_firmware_upgrader/tests/test_models.py`:
- Around line 606-608: Replace hard-coded app-label string patching with
model-bound patching: import or reference the UpgradeOperation model class and
use mock.patch.object(UpgradeOperation, "upgrade", return_value=None) instead of
mock.patch(f"{self.app_label}.models.UpgradeOperation.upgrade", ...); do the
same for the other occurrence that patches UpgradeOperation.upgrade (the second
instance mentioned) so tests remain resilient to model swapping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4eaa450b-cfb3-458c-8956-a3dc2e9dbe2a

📥 Commits

Reviewing files that changed from the base of the PR and between f9f1e6a and 6595d4f.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (3)
📚 Learning: 2026-02-21T20:21:02.014Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
📚 Learning: 2026-02-23T21:36:22.028Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_models.py
🔇 Additional comments (4)
openwisp_firmware_upgrader/tests/test_models.py (1)

586-602: LGTM!

Also applies to: 609-630, 634-673

openwisp_firmware_upgrader/base/models.py (1)

461-461: LGTM!

Also applies to: 571-579, 846-846, 870-894, 896-913

openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_persistent_and_more.py (1)

13-67: LGTM!

tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_persistent_and_more.py (1)

13-67: LGTM!

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_models.py Outdated
@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 6595d4f to 4dba3f9 Compare May 23, 2026 11:43

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking good, please batch in more work or it will be hard for me to get an idea of whether we're really going in the right direction.

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
("failed", _("failed")), # failed at late stage or can't reconnect
("cancelled", _("cancelled")), # cancelled by the user
("aborted", _("aborted")), # aborted due to prerequisites not met
("pending", _("pending")), # offline device; waiting for periodic retry

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure this is cancellable and add a test for this: pending upgrades can be cancled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment thread openwisp_firmware_upgrader/base/models.py Outdated

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would include the minimum behavior that makes the new state coherent:

  • BatchUpgradeOperation.progress_report: pending must not be counted as completed.
  • BatchUpgradeOperation.calculate_and_update_status(): a batch with pending child operations should remain active, not become success.
  • UpgradeOperation.cancel(): pending operations should be cancellable.
  • UpgradeOperation.upgrade(): an existing pending operation should block a second upgrade for the same device, the same way in-progress does.

I would also update the BatchUpgradeOperation.clean() immutability check to use the stored database status, not self.status, otherwise a caller can set status="idle" and change is_persistent in the same edit.

Please rename persistent to is_persistent consistently on both BatchUpgradeOperation and UpgradeOperation.

For tests, I would add a batch with a mix of success and pending child operations. The expected behavior should be: progress does not count pending as completed, batch status remains active, pending operation can be cancelled, and another upgrade for the same device is blocked while the pending operation exists.

@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch 2 times, most recently from 45fad3d to 6d84175 Compare May 26, 2026 15:48
@openwisp-companion

Copy link
Copy Markdown

Multiple Test Failures Detected

Hello @Eeshu-Yadav,
(Analysis for commit b32e764)

There are multiple test failures in your commit:

  1. Test Failures: Several tests failed, indicated by FAILED (failures=2, errors=1). The specific tests that failed are listed in the logs, including test_save_device_after_credentials_deleted, test_save_device_with_deleted_devicefirmware, and test_api_batch_upgrade_list_filter_org.

Fix: Please review the failing tests and their corresponding code to identify the root cause of the failures. Common issues include incorrect test setup, assertion errors, or unexpected behavior in the code being tested.

  1. Syntax Warnings: Multiple SyntaxWarning: invalid escape sequence warnings were reported in openwisp_firmware_upgrader/upgraders/openwrt.py and openwisp_firmware_upgrader/tests/test_openwrt_upgrader.py. These warnings indicate improperly escaped backslashes in regular expressions or string literals.

Fix: Escape backslashes in string literals and regular expressions by using double backslashes (\\) or by using raw strings (e.g., r'...'). For example, r'\{' or '\\{'.

@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from b32e764 to f910f6a Compare May 26, 2026 19:17
@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@nemesifier

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from f910f6a to 741148f Compare May 27, 2026 05:44
name="next_retry_at",
field=models.DateTimeField(
blank=True,
db_index=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This migration adds a blocking index on upgradeoperation.next_retry_at

AddField(... db_index=True) will create the index with regular DDL during the deploy. On populated installations that can block writes to upgradeoperation, so this index should be moved into a separate non-atomic concurrent migration, for example with AddIndexConcurrently.

Comment thread openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js Outdated
@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 741148f to 509a56a Compare May 27, 2026 06:04
@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's going in the right direction, next steps:

  • admin
  • docs: start adding a mention of this feature in the intro page, let's add a dedicated page for persistent upgrades

Keep pushing to this branch, I need to see the whole thing before I am confident in merging, if I merge now, reviewing the diff for other fragments will hide details that make my life as a reviewer harder. Go ahead, keep iterating, pushing, advancing.

Update the PR title and description when due, add screenshots when there's UI changes.

batch_status,
stats["completed"],
stats["total_operations"],
stats["pending"],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if the docs need to be updated due to this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes docs updated related to this

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
completedInfo.text(`${data.completed} complete, ${pending} pending`);
} else {
completedInfo.text(`${data.completed} out of ${data.total}`);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add assertions in the existing selenium tests for this logic please

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

)
PERSISTENT_RETRY_DISPATCH_JITTER = getattr(
settings, "OPENWISP_FIRMWARE_UPGRADER_PERSISTENT_RETRY_DISPATCH_JITTER", 300
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need to be documented. Are you sure we can't add one setting with a dictionary for all of them to avoid bloating the docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated it

CELERY_BROKER_URL = "memory://"
CELERY_RESULT_BACKEND = "cache+memory://"

CELERY_BEAT_SCHEDULE = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have issues in ansible-openwisp2 and docker-openwisp to add these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have opened the issues in the ansible and docker repos :
openwisp/docker-openwisp#623
openwisp/ansible-openwisp2#621

Comment thread docs/user/upgrade-status.rst Outdated
Comment thread docs/index.rst

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's going very well, let's continue iterating as we find details to refine.

The log lines are a bit inconsistent:

Image

We need each log line related to persistent retries to stand on its own, e g:

The upgrade operation will be retried soon.
Failed to connect with AX820-Fed-Staging using SSH Key (SSH). Error: timed out
All immediate retries exhausted: Failed to establish connection with the device, tried all DeviceConnections.

Scheduled persistent retry #1 at 2026-07-09 16:43:32.102524+00:00.

Persistent retry #1 starting.

Failed to connect with AX820-Fed-Staging using SSH Key (SSH). Error: timed out
Detected a recoverable failure: Failed to establish connection with the device, tried all DeviceConnections.

The upgrade operation will be retried soon.
Failed to connect with AX820-Fed-Staging using SSH Key (SSH). Error: timed out
Detected a recoverable failure: Failed to establish connection with the device, tried all DeviceConnections.

The upgrade operation will be retried soon.
Failed to connect with AX820-Fed-Staging using SSH Key (SSH). Error: timed out
Detected a recoverable failure: Failed to establish connection with the device, tried all DeviceConnections.

The upgrade operation will be retried soon.
Failed to connect with AX820-Fed-Staging using SSH Key (SSH). Error: timed out
Detected a recoverable failure: Failed to establish connection with the device, tried all DeviceConnections.

The upgrade operation will be retried soon.
Failed to connect with AX820-Fed-Staging using SSH Key (SSH). Error: timed out
All immediate retries exhausted: Failed to establish connection with the device, tried all DeviceConnections.

Scheduled persistent retry #2 at 2026-07-09 17:17:38.648201+00:00.

Comment thread openwisp_firmware_upgrader/apps.py Outdated
Comment thread CHANGES.rst Outdated
Comment thread openwisp_firmware_upgrader/tests/test_notifications.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_notifications.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_handlers.py
The pending-reminder and persistent-failure notifications used two custom
notification types (pending_upgrade_reminder, persistent_upgrade_failed).
#426 asks for the built-in generic_message type instead, so both now send
generic_message. The clean title (otherwise "Generic notification with
generic verb and level info by ...") is kept by passing message alongside
description, which get_message() prefers over the type's default template.

The reminder also passes a url to the batch change page filtered with
?status=pending, so admins land directly on the devices still waiting.

Related to #379
… migrations #379

next_retry_at (0018) keeps db_index=True: check_pending_upgrades scans
status="pending", next_retry_at <= now every 10 minutes on the per-device
UpgradeOperation table, and that btree is the query's only usable, highly
selective index (next_retry_at is null for every non-pending op). The header
documents the write-blocking build: the non-concurrent CREATE INDEX takes a
SHARE lock (reads keep working), so on a large table run the migration in a
maintenance window or pre-build with CREATE INDEX CONCURRENTLY.

last_reminder_at (0019) drops the index instead: the reminder scan runs about
weekly against the small BatchUpgradeOperation table and its OR/IS-NULL due
filter cannot use a single-column btree anyway, so an index there would only
add build cost. Both migrations are mirrored in the sample app.

Related to #379
The retry-flow detail lived in both upgrade-status and the dedicated
persistent-mass-upgrades page; upgrade-status now keeps a short pointer
and links out. The intro page and the module landing page mention
persistent upgrades and link to the dedicated page, and the settings page
refers to the reminder as a generic_message notification.

Related to #379
…tegration

The persistent mass-upgrade reminder relies on openwisp-notifications
honoring the per-notification "url" for the web notification link, so the
"Open" button lands on the batch filtered to its pending devices. That
support lands in openwisp/openwisp-notifications#490; pin the dependency to
that commit so the integration is reproducible until it is released.
@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 7d97360 to a56e5aa Compare July 11, 2026 19:01
@Eeshu-Yadav

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Line 1143: Update the display formatting in the surrounding string
representation to pass self.next_retry_at through timezone.localtime(...) before
calling strftime, matching the existing local-time formatting conventions while
preserving the current output format.

In
`@openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css`:
- Around line 22-42: Merge the two fieldset.aligned label.is-persistent-checkbox
declarations into one rule, preserving all existing properties and resolving the
final padding values as padding-top: 0 and padding-bottom: 0.

In `@openwisp_firmware_upgrader/tests/test_models.py`:
- Around line 835-842: The non-persistent unreachable-upgrade path in the
`error.connection is None` branch of `upgrade` must transition to a terminal
failure state instead of remaining in-progress. Update the status handling in
`upgrade` accordingly, ensure the associated batch is no longer left active, and
revise `test_no_connection_non_persistent_op_stays_in_progress` to assert the
new terminal behavior.

In `@openwisp_firmware_upgrader/tests/test_tasks.py`:
- Around line 64-73: Update the upgrade_firmware flow to re-check or atomically
claim the operation immediately before invoking UpgradeOperation.upgrade,
preventing a cancellation between the initial status check and flashing; add a
test covering cancellation during that window and assert upgrade is not called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: afc9b399-69a2-406d-8cc4-d383cad6b185

📥 Commits

Reviewing files that changed from the base of the PR and between ca39bb8 and 87d1206.

📒 Files selected for processing (37)
  • CHANGES.rst
  • docs/index.rst
  • docs/user/intro.rst
  • docs/user/persistent-mass-upgrades.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/upgrade-status.rst
  • docs/user/websocket-api.rst
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js
  • openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/websockets.py
  • requirements-test.txt
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/settings.py
💤 Files with no reviewable changes (1)
  • CHANGES.rst
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
docs/**/*.rst

📄 CodeRabbit inference engine (AGENTS.md)

Update docs when behavior, settings, public APIs, setup steps, or supported versions change

Files:

  • docs/user/websocket-api.rst
  • docs/user/intro.rst
  • docs/index.rst
  • docs/user/settings.rst
  • docs/user/rest-api.rst
  • docs/user/upgrade-status.rst
  • docs/user/persistent-mass-upgrades.rst
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Mark user-facing strings for translation with Django i18n helpers in Django code
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/websockets.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework.

Files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/websockets.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
🧠 Learnings (8)
📚 Learning: 2026-02-21T20:21:02.014Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:02.014Z
Learning: In Django migrations within this repo, avoid adding explicit related_name to ForeignKey fields when the default reverse accessor (<model>_set) is acceptable. Rely on Django’s default naming convention to keep migrations concise and maintainable. Only specify related_name if you have a concrete, needed override for reverse relations.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
📚 Learning: 2026-02-23T21:36:22.028Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:22.028Z
Learning: In the sample_firmware_upgrader app within tests, avoid creating new migration files for small changes since the sample is for demonstration and testing. Review migrations with this context: the sample's initial migration may differ from the main app and need not be strictly aligned. If you change models for the demo, keep migrations minimal and document why divergence exists; don’t require parity with the main app migrations in PRs focused on the sample.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
📚 Learning: 2026-06-02T08:16:00.439Z
Learnt from: asmodehn
Repo: openwisp/openwisp-firmware-upgrader PR: 362
File: tests/openwisp2/urls.py:10-38
Timestamp: 2026-06-02T08:16:00.439Z
Learning: In this repository, treat `SAMPLE_APP` as a conventionally truthy environment variable: code should use the established pattern `os.environ.get("SAMPLE_APP", False)` and rely on the resulting value’s truthiness (truthy string → enabled). During code reviews, avoid recommending changes to explicit boolean parsing (e.g., checking for `

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/settings.py
  • openwisp_firmware_upgrader/api/views.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/apps.py
  • openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/websockets.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tasks.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/api/serializers.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_api.py
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-06-15T20:19:28.326Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 437
File: openwisp_firmware_upgrader/tests/test_admin.py:1072-1147
Timestamp: 2026-06-15T20:19:28.326Z
Learning: In this codebase (OpenWISP projects, including openwisp-firmware-upgrader), prefer test methods that are fully self-contained: each test should do its own setup and make its own explicit assertions within the test body. Avoid introducing shared assertion helpers or parameterized helper methods that abstract away assertions across tests, even if it leads to some duplication.

Applied to files:

  • tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py
  • tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py
  • openwisp_firmware_upgrader/tests/test_notifications.py
  • tests/openwisp2/settings.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_api.py
📚 Learning: 2026-02-23T20:09:13.275Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: requirements-test.txt:1-1
Timestamp: 2026-02-23T20:09:13.275Z
Learning: When reviewing OpenWISP projects (e.g., openwisp-firmware-upgrader), do not flag floating branch references like refs/heads/1.3 in requirements files as a violation. This project workflow intentionally uses development-version refs for openwisp-utils dependencies and should not require pinning to a specific commit SHA or tag. If you encounter such refs, treat them as acceptable in requirements-*.txt files and verify other aspects (e.g., compatibility) through tests rather than pinning. 

Applied to files:

  • requirements-test.txt
📚 Learning: 2026-02-24T00:04:04.187Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:04.187Z
Learning: In openwisp-firmware-upgrader, prefer hiding empty submit rows via CSS (e.g., `#upgradeoperation_form` .submit-row { display: none; }) rather than removing elements in templates. This keeps template logic simple and can improve rendering performance. Apply this pattern to CSS files in openwisp_firmware_upgrader/static/firmware-upgrader/css/*.css for consistent behavior across upgrade-option related styles.

Applied to files:

  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css
  • openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-selected-confirmation.css
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_notifications.py
  • openwisp_firmware_upgrader/tests/base.py
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/tests/test_websockets.py
  • openwisp_firmware_upgrader/tests/test_handlers.py
  • openwisp_firmware_upgrader/tests/test_tasks.py
  • openwisp_firmware_upgrader/tests/test_models.py
  • openwisp_firmware_upgrader/tests/test_selenium.py
  • openwisp_firmware_upgrader/tests/test_api.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
🪛 ast-grep (0.44.1)
tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py

[info] 69-80: use help_text to document model columns
Context: models.CharField(
choices=[
("in-progress", "in progress"),
("success", "success"),
("failed", "failed"),
("cancelled", "cancelled"),
("aborted", "aborted"),
("pending", "pending"),
],
default="in-progress",
max_length=12,
)
Note: [CWE-710] Improper Adherence to Coding Standards.

(model-help-text)

openwisp_firmware_upgrader/tests/test_notifications.py

[warning] 12-12: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 13-13: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("BatchUpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_firmware_upgrader/apps.py

[warning] 130-130: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("device_monitoring", "DeviceMonitoring", required=False)
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 134-134: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("firmware_upgrader", "UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py

[info] 69-80: use help_text to document model columns
Context: models.CharField(
choices=[
("in-progress", "in progress"),
("success", "success"),
("failed", "failed"),
("cancelled", "cancelled"),
("aborted", "aborted"),
("pending", "pending"),
],
default="in-progress",
max_length=12,
)
Note: [CWE-710] Improper Adherence to Coding Standards.

(model-help-text)

openwisp_firmware_upgrader/tasks.py

[info] 175-175: use secrets package over random package
Context: random.uniform(0, jitter)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)


[warning] 118-118: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 160-160: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 181-181: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("BatchUpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_firmware_upgrader/tests/test_handlers.py

[warning] 14-14: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_firmware_upgrader/base/models.py

[warning] 481-481: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 666-666: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("BatchUpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 987-987: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("UpgradeOperation")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[info] 1078-1078: use secrets package over random package
Context: random.uniform(1 - jitter, 1 + jitter)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)


[info] 1098-1098: use secrets package over random package
Context: random.uniform(0, jitter)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🔇 Additional comments (53)
docs/index.rst (1)

13-15: LGTM!

Also applies to: 44-44

docs/user/intro.rst (1)

14-15: LGTM!

docs/user/persistent-mass-upgrades.rst (1)

1-120: LGTM!

docs/user/rest-api.rst (1)

222-223: LGTM!

Also applies to: 323-324, 365-365, 381-384

docs/user/settings.rst (1)

39-46: LGTM!

Also applies to: 57-121

docs/user/upgrade-status.rst (1)

36-62: LGTM!

Also applies to: 139-147, 188-196, 224-233

docs/user/websocket-api.rst (1)

139-139: LGTM!

Also applies to: 184-184

tests/openwisp2/settings.py (1)

3-3: LGTM!

Also applies to: 191-201

openwisp_firmware_upgrader/migrations/0018_batchupgradeoperation_is_persistent_and_more.py (4)

3-8: Blocking-index concern already mitigated with documented maintenance-window warning.

The next_retry_at field still uses db_index=True, but the file now carries an explicit warning (added in response to the earlier review) that the non-concurrent CREATE INDEX can block writes on large tables and recommends a maintenance window or pre-building the index concurrently. This directly addresses the earlier blocking-index concern raised by kilo-code-bot and nemesifier.

Also applies to: 47-57


20-37: LGTM!


38-66: LGTM!


67-82: LGTM!

openwisp_firmware_upgrader/migrations/0019_batchupgradeoperation_last_reminder_at.py (1)

13-22: LGTM! The db_index=True flagged by kilo-code-bot in an earlier revision has been removed, resolving the blocking-index concern for this field.

tests/openwisp2/sample_firmware_upgrader/migrations/0005_batchupgradeoperation_is_persistent_and_more.py (1)

1-84: LGTM! Mirrors the main-app migration 0018_batchupgradeoperation_is_persistent_and_more.py, including the backfill pattern and the maintenance-window warning, as previously requested.

tests/openwisp2/sample_firmware_upgrader/migrations/0006_batchupgradeoperation_last_reminder_at.py (1)

1-27: LGTM! Correctly mirrors the main-app migration without db_index=True, consistent with the resolved discussion on 0019.

openwisp_firmware_upgrader/tests/test_api.py (5)

671-694: LGTM! Correctly verifies is_persistent defaults to True when omitted and honors explicit overrides, matching batch_upgrade's is_persistent=True default.


1044-1063: LGTM!


1494-1515: LGTM! Consistent with AbstractDeviceFirmware.save's is_persistent=False default and the serializer's create() popping is_persistent.


1969-1992: LGTM!


2186-2237: LGTM! Cancellation and immutability-validation behavior match the upstream cancel()/_validate_is_persistent_immutable contracts.

openwisp_firmware_upgrader/tests/test_selenium.py (4)

278-311: LGTM! Verifies the persistent checkbox default state and propagation to child operations, matching PR objective #417/#427.


378-411: LGTM!


499-538: LGTM! Polling the DB via WebDriverWait for the async status transition is a sound pattern to avoid flakiness.


1102-1175: LGTM! The pending-count text switching and orange-indicator assertions are consistent with BatchUpgradeProgressPublisher.publish_batch_status's pending parameter and the UpgradeOperation.save() trigger for batch recalculation.

requirements-test.txt (1)

2-2: LGTM! Pinning openwisp-notifications to a specific commit SHA (rather than a floating branch ref) is good practice for reproducible test builds, needed for the new failure/pending-reminder notification tests.

openwisp_firmware_upgrader/tests/test_models.py (2)

819-829: Use the loaded DeviceConnection model for both patches.

These concrete paths can miss a swapped connection model. Patch DeviceConnection.get_working_connection with mock.patch.object instead.

Based on learnings, model patches in openwisp_firmware_upgrader/tests should remain swap-safe rather than use hard-coded model paths.

Also applies to: 924-927

Source: Learnings


10-27: LGTM!

Also applies to: 594-818, 844-923, 928-1019, 1098-1107

openwisp_firmware_upgrader/tests/base.py (1)

2-2: LGTM!

Also applies to: 11-11, 27-32

openwisp_firmware_upgrader/tests/test_tasks.py (1)

1-14: LGTM!

Also applies to: 84-282

openwisp_firmware_upgrader/tests/test_websockets.py (1)

403-408: LGTM!

Also applies to: 705-787

openwisp_firmware_upgrader/tests/test_notifications.py (1)

1-207: LGTM!

openwisp_firmware_upgrader/tests/test_handlers.py (1)

1-210: LGTM!

openwisp_firmware_upgrader/tests/test_admin.py (1)

203-283: LGTM!

Also applies to: 2131-2177

openwisp_firmware_upgrader/base/models.py (5)

1132-1148: 🎯 Functional Correctness | ⚡ Quick win

Untranslated log strings remain in _recoverable_failure_handler.

Both the recoverable branch ("Detected a recoverable failure...", "The upgrade operation will be retried soon.") and the persistent branch ("All immediate retries exhausted...", "Scheduled persistent retry #...") still use raw f-strings, unlike the sibling fix already applied to the "No device connection available" message at Line 1163/1170. Past review comments flagged this exact gap without a recorded fix.

🌐 Proposed fix
     def _recoverable_failure_handler(self, recoverable, error):
         cause = str(error)
         if recoverable:
-            self.log_line(f"Detected a recoverable failure: {cause}.\n", save=False)
-            self.log_line("The upgrade operation will be retried soon.")
+            self.log_line(
+                _("Detected a recoverable failure: %(cause)s.\n") % {"cause": cause},
+                save=False,
+            )
+            self.log_line(_("The upgrade operation will be retried soon."))
             raise error
         if self.is_persistent and isinstance(error, RecoverableFailure):
             self.status = "pending"
             self.retry_count += 1
             self.next_retry_at = self._calculate_next_retry()
-            self.log_line(f"All immediate retries exhausted: {cause}.\n", save=False)
-            retry_time = self.next_retry_at.strftime("%Y-%m-%d %H:%M:%S %Z")
-            self.log_line(
-                f"Scheduled persistent retry #{self.retry_count} at {retry_time}.\n",
-                save=False,
-            )
+            self.log_line(
+                _("All immediate retries exhausted: %(cause)s.\n") % {"cause": cause},
+                save=False,
+            )
+            retry_time = timezone.localtime(self.next_retry_at).strftime(
+                "%Y-%m-%d %H:%M:%S %Z"
+            )
+            self.log_line(
+                _("Scheduled persistent retry #%(count)s at %(time)s.\n")
+                % {"count": self.retry_count, "time": retry_time},
+                save=False,
+            )
             return

As per coding guidelines, **/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code.

Source: Coding guidelines


2-3: LGTM!

Also applies to: 17-17, 44-44, 175-210, 452-497


601-618: LGTM!

Also applies to: 656-681, 746-770, 840-925


949-1000: LGTM!

Also applies to: 1045-1051, 1071-1081, 1102-1130, 1161-1170, 1202-1202


1082-1100: 🗄️ Data Integrity & Integration

Same-device concurrency race review — deferring to consolidated comment.

handle_health_status_changed dispatches retries for any operation with status="pending" without also constraining is_persistent=True. This shares the same root cause as the send_pending_upgrade_reminders finding in tasks.py — see the consolidated comment at the end of this review.

openwisp_firmware_upgrader/tasks.py (4)

180-222: 🗄️ Data Integrity & Integration

Reminder queryset can match non-persistent batches — deferring to consolidated comment.

The batch queryset filters only on status="in-progress" plus a pending child, without is_persistent=True. See the consolidated comment at the end of this review for the shared root cause and fix across files.


31-55: LGTM!


115-157: LGTM!


159-178: LGTM!

openwisp_firmware_upgrader/settings.py (1)

27-41: LGTM!

openwisp_firmware_upgrader/apps.py (1)

32-32: LGTM!

Also applies to: 93-97, 125-140

openwisp_firmware_upgrader/static/firmware-upgrader/js/batch-upgrade-progress.js (1)

203-216: LGTM!

openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-progress.css (1)

52-55: LGTM!

openwisp_firmware_upgrader/admin.py (1)

128-139: LGTM!

Also applies to: 227-253, 397-404, 413-441, 517-528, 538-555, 809-809

openwisp_firmware_upgrader/api/serializers.py (1)

88-91: LGTM!

Also applies to: 106-116, 121-133, 160-181

openwisp_firmware_upgrader/api/views.py (1)

98-106: LGTM!

Also applies to: 157-157

openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

50-55: LGTM!

openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-utils.js (1)

10-10: LGTM!

Also applies to: 20-20, 35-35, 63-69, 86-86, 99-103

openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js (1)

252-255: LGTM!

Also applies to: 294-295

openwisp_firmware_upgrader/websockets.py (1)

254-259: LGTM!

Also applies to: 268-268, 336-336, 506-512, 525-525

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_models.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_tasks.py Outdated
@Eeshu-Yadav Eeshu-Yadav moved this from In progress to In review in [GSoC26] Persistent & Scheduled Firmware Upgrades Jul 15, 2026
- Surface the persistent-retry state (is_persistent, retry_count,
  next_retry_at) above the log on the operation page so the state is
  visible without scrolling past the log.
- Make each persistent-retry log line stand on its own with a clean,
  local-time timestamp.
- Verify the monitoring signal wiring by emission and use
  openwisp_utils.tests.catch_signal for signal-emission tests.
- Merge the duplicate is-persistent-checkbox CSS rule on the mass-upgrade
  confirmation page.
- Tidy the monitoring docstring and drop the manual changelog entry
  (the releaser manages it).

Related to #379
@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 87d1206 to ee7a610 Compare July 15, 2026 07:57
@kilo-code-bot

kilo-code-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

Kilo Code Review could not run — your account is out of credits.

Add credits or switch to a free model to enable reviews on this change.

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one remaining race that should be fixed before merging.

Comment thread openwisp_firmware_upgrader/tasks.py Outdated
… race #417

upgrade_firmware() checked the operation status in the database, then called
upgrade() on the already-loaded instance. A cancellation issued after that
check but before upgrade() ran left the stale instance marked in-progress, so
upgrade() proceeded into the flashing path and overwrote the cancellation.
Refresh the instance immediately before calling upgrade() so its existing
in-memory guard observes the cancellation, and drop the now-redundant status
query.

Related to #417
@Eeshu-Yadav
Eeshu-Yadav force-pushed the issues/417-persistence-schema-fields branch from 1deab78 to 49c90c2 Compare July 17, 2026 11:28
…417

When no working connection existed, a non-persistent operation only
logged the problem and returned, leaving it in-progress forever and
blocking any future upgrade of the same device. Move it to the aborted
status instead, since a device without a usable connection cannot be
upgraded and its prerequisites are not met.

Related to #417

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am looking into upgrade operations not flagged as persistent, eg:

Image

From an old test did in the staging environment (you have access to this).

When persistent is False, there's no point in showing retry count and next retry as it only confuses users, it should be possible to do so in the admin.py code without involving JS/CSS.

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove also retry_count from the list page of the upgrade operations please.

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a persistent mas upgrade, I think it would be beneficial to see the retry_count in the list of upgrades:

Image

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two retry-state correctness issues that should be fixed before merging.

if operation.device.is_deactivated():
aborted = UpgradeOperation.objects.filter(
pk=operation_id, status="in-progress"
).update(status="failed")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stores the operation as failed while the user-facing log says the persistent retry was aborted. That is inconsistent in the admin/API and also triggers failed-persistent-upgrade behavior for a device deactivation path. Please either store aborted here, which matches the log and existing deactivated-device semantics, or change the log and downstream expectations if failed is intentional.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS: in master this case is flagged as aborted, let's maintain consistency.

def retry_pending_upgrade(operation_id):
UpgradeOperation = load_model("UpgradeOperation")
updated = UpgradeOperation.objects.filter(pk=operation_id, status="pending").update(
status="in-progress"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this pending retry is claimed, next_retry_at remains populated even though the operation is no longer pending and no retry is queued for that timestamp. The admin and REST API then show a stale next retry date while the operation is in-progress. Please clear next_retry_at when moving the operation back to in-progress.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS: added a failing test for this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PPS: can we avoid showing retry_next at all if it's None? I think this would be better.

@openwisp-companion

Copy link
Copy Markdown

Test Failure in test_retry_pending_upgrade_happy_path

Hello @Eeshu-Yadav and @nemesifier,
(Analysis for commit d71a182)

The test test_retry_pending_upgrade_happy_path failed with an AssertionError. The assertion self.assertIsNone(op.next_retry_at) failed because op.next_retry_at was not None, but instead a datetime object.

This indicates that the next_retry_at field was expected to be cleared after a successful retry, but it was not.

To fix this:

Review the logic in the retry_pending_upgrade task and the UpgradeOperation model to ensure that next_retry_at is correctly reset or handled when an upgrade operation is retried successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request python Pull requests that update Python code

Projects

Development

Successfully merging this pull request may close these issues.

3 participants