Skip to content

[feature] Added X.509 Certificate Generator Templates#1378

Open
stktyagi wants to merge 70 commits into
gsoc26-x509-certificate-generator-templatesfrom
issues/1356-extend-abstract-template
Open

[feature] Added X.509 Certificate Generator Templates#1378
stktyagi wants to merge 70 commits into
gsoc26-x509-certificate-generator-templatesfrom
issues/1356-extend-abstract-template

Conversation

@stktyagi

@stktyagi stktyagi commented May 26, 2026

Copy link
Copy Markdown
Member

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 #1356
Closes #1377
Closes #1357
Closes #1361
Closes #1358
Closes #1360
Closes #1359

Description of Changes

This PR establishes the database architecture, UI, API and lifecycle for standalone X.509 certificate templates.

Manual test plan

Setup

  • Go to PKI -> Certification Authorities and create two CAs: CA-1 and CA-2.
  • Go to PKI -> Certificates and create two certificates to act as blueprints:
  • Blueprint-1 (Must use CA-1)
  • Blueprint-2 (Must use CA-2)
  • Go to Devices and create a device (test-device).

Template Creation and Validation

  • Configuration -> Templates and click ADD TEMPLATE.
  • Set Type to Certificate.
    • Leave CA blank and try to save.
    • Expected Result: Validation error stating a CA is required.
  • Set CA to CA-1.
    • Set Blueprint to Blueprint-2 (which belongs to CA-2). Try to save.
    • When opening drop-down for blueprint you'll only see unassigned and unrevoked certificates.
    • Expected Result: Validation error stating the Blueprint must match the selected CA.
  • Change Blueprint to Blueprint-1. Name the template Active-Cert-Template. Save it.

Device Provisioning

  • Add configuration for test-device.
  • In the templates field, add Active-Cert-Template. Save.
  • Go to PKI -> Certificates.
  • Expected Result: You should see a brand new certificate automatically generated for test-device. Its status should be valid (not revoked).

Active Mutation Locks

  • Go back to Configuration -> Templates and edit Active-Cert-Template (which is now assigned to an active device).
  • Change the Type to Generic. Try to save.
  • Expected Result: Validation error: "You cannot change the template type from certificate on an active template."
  • Change the CA to CA-2. Try to save.
  • Expected Result: Validation error blocking the CA change.
  • Change the Blueprint to Blueprint-2 (ensure you also change the CA so they match, triggering the active lock). Try to save.
  • Expected Result: Validation error blocking the Blueprint change.

Revocation on Removal

  • Go to the Configuration for test-device.
  • Remove Active-Cert-Template entirely from the templates list. Save.
  • Go to PKI -> Certificates and locate the device's certificate.
  • Expected Result: The certificate should still exist in the database, but its status should now be marked as Revoked.

Context Configuration Injection

  • Go to Configuration -> Templates, open Active-Cert-Template, and copy its UUID from the URL bar (removing the dashes so it is a 32-character hex string).

  • In the JSON configuration editor for the template, add a configuration block that references the certificate's UUID variables:

    {
        "files": [
            {
                "path": "{{ cert_<uuid>_path }}",
                "mode": "0600",
                "contents": "{{ cert_<uuid>_pem }}"
            }
        ]
    }
    

    (Note: Replace <uuid> with the actual 32-character hex string of the template).

  • Click Save.

  • Go back to the Configuration page for test-device (which has this template assigned) and click the Preview configuration button.

  • Expected Result: The variables should be successfully resolved. In the preview, you should see the generated path (e.g., /etc/x509/cert-<uuid>.pem) and the literal -----BEGIN CERTIFICATE----- text instead of the raw {{ }} template tags.

output.mp4

…1356

- Added 'cert' to TYPE_CHOICES.
- Introduced 'ca' and 'blueprint_cert' ForeignKeys with organization validation.
- Updated the clean() method to clear unneeded relations, require a CA for cert types, and validate that a blueprint certificate is not already assigned to a device.

Fixes #1356
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

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: b8ae1223-214d-411d-9113-00dea1b585b1

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 standalone cert templates with CA and blueprint certificate support, validation, and persistence. It introduces DeviceCertificate lifecycle management for assignment, generation, revocation, renewal, context injection, and hardware-drift regeneration. The API, admin UI, JavaScript behavior, migrations, tests, and documentation are updated to support certificate provisioning and management.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeviceConfigSerializer
  participant AbstractConfig
  participant manage_device_certs
  participant DeviceCertificate
  participant ConfigContext

  DeviceConfigSerializer->>AbstractConfig: update templates
  AbstractConfig->>manage_device_certs: process template assignment
  manage_device_certs->>DeviceCertificate: create or delete certificate rows
  DeviceCertificate->>AbstractConfig: update certificate state
  AbstractConfig->>ConfigContext: inject certificate context
Loading
sequenceDiagram
  participant Device
  participant detect_hardware_drift
  participant regenerate_device_certificates_task
  participant DeviceCertificate
  participant Config

  Device->>detect_hardware_drift: save name or MAC change
  detect_hardware_drift->>regenerate_device_certificates_task: schedule regeneration
  regenerate_device_certificates_task->>DeviceCertificate: revoke and replace certificates
  regenerate_device_certificates_task->>Config: update status after commit
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: nemesifier, devkapilbansal, codesankalp

🚥 Pre-merge checks | ✅ 4 | ❌ 4

❌ Failed checks (4 inconclusive)

Check name Status Explanation Resolution
Bug Fixes ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Features ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Changes ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
General Rules ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required prefix and accurately summarizes the new X.509 certificate template feature.
Description check ✅ Passed The description is clearly about standalone X.509 certificate templates and matches the code changes.
Linked Issues check ✅ Passed The changes cover the listed template, model, admin, API, lifecycle, context, and regeneration requirements.
Out of Scope Changes check ✅ Passed I don't see unrelated code; the extra docs, tests, migrations, and UI updates all support the certificate-template feature.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/1356-extend-abstract-template

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.

@stktyagi

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
✅ 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 26, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (9 files)
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/subnet_division/tasks.py
  • openwisp_controller/subnet_division/tests/test_models.py
Previous Review Summaries (11 snapshots, latest commit 1d117a2)

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

Previous review (commit 1d117a2)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since 375844d)

This delta is a test-only refactor in openwisp_controller/config/tests/test_device.py. It mixes in TestPkiMixin (from openwisp_controller/pki/tests/utils.py) and replaces every Ca.objects.create(name=..., organization=...) call with self._create_ca(name=..., organization=...), matching how the rest of the file already creates Cert objects. The helper runs full_clean() for proper validation and forwards name/organization via **kwargs, so all call sites remain valid and behavior is preserved. No critical bugs or security vulnerabilities.

Files Reviewed (incremental — 1 file)
  • openwisp_controller/config/tests/test_device.py

Previous review (commit 375844d)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since c12aee9)

This delta refactors the hardware-drift detection (name/MAC change → certificate regeneration) to live entirely in the model rather than a separate pre_save handler. The change is clean, behavior-preserving, and well-tested. No critical bugs or security vulnerabilities.

  • config/base/device.py — Adds mac_address to _changed_checked_fields, so _initial_mac_address is now captured in __init__ (via _set_initial_values_for_changed_checked_fields) and refreshed for deferred fields. Adds _check_mac_address_changed(), mirroring _check_name_changed() (deferred-guard, compare, set_status_modified() on change, then reset initial). Also stores _initial_name at the end of _check_name_changed. Consistent with the existing change-tracking pattern.
  • config/handlers.py — Removes the pre_save capture_old_hardware_properties receiver and the _old_name/_old_mac/_hardware_field_was_saved machinery; detect_hardware_drift (post_save) now reads instance._initial_name / instance._initial_mac_address with models.DEFERRED guards and the renamed _field_was_saved helper. This addresses the maintainer's prior feedback to keep the logic in the model and reduce extra queries.

Ordering is correct: post_save fires inside super().save() before _check_changed_fields() resets the _initial_* values, so detect_hardware_drift observes the pre-save initial values. No stale references to the removed symbols remain.

Tests cover the refactored paths: name change, MAC change, unrelated-field save (ignored), REGENERATE_CERTS_ON_HARDWARE_CHANGE=False, and partial update_fields=["name"] save ignoring dirty in-memory MAC (test_hardware_drift_signal_triggers_on_name_change, ..._on_mac_change, ..._ignores_unrelated_changes, ..._setting_disables_regeneration, ..._partial_save_ignores_dirty_memory).

Files Reviewed (incremental — 2 files)
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/handlers.py

Previous review (commit c12aee9)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since b9a9d2a)

Since the last review, the branch merged the base branch (gsoc26-x509-certificate-generator-templates) twice, bringing in unrelated repo maintenance (CI workflows, PR template, publiccode.yml, logo SVGs, dependency bumps, and a sample_users migration). Those merge-in changes are not part of this PR's feature work and contain no critical bugs or security issues.

The author's own changes in this delta are clean and well-tested:

  • config/base/template.py — Adds a notes TextField (blank=True) and rejects a revoked blueprint_cert in clean(). Covered by tests.
  • config/base/device_certificate.py — Extracts the active auto-cert queryset into active_auto_certs_for(device); simplifies digest = str(source.digest). Behavior-preserving.
  • config/handlers.py — Adds raw guards to the hardware-drift signal handlers (correctly skips fixture/loaddata saves) and reuses active_auto_certs_for. Query is equivalent to the previous inline filter.
  • config/tasks.py — Reuses active_auto_certs_for(...).select_for_update(); replaces the redundant except (ImportError, Exception) with except Exception.
  • config/api/serializers.py — Exposes notes; hoists the cert_fields_provided flag with unchanged logic. API tests updated (create/list/patch, field count 15—16).
  • config/admin.py + templates — Moves certificate details from a readonly inline field into a dedicated context-injected "Certificate details" fieldset, guarded by pk and hasattr(device, 'config') and device.config (safe on add view), with a bounded query ([:51], capped at 50 + "view all" link). Admin test updated.
  • config/static/config/js/switcher.js — Removes a duplicated auto_cert toggle block; remaining logic intact.
  • Migrations0064_template_notes (nullable-safe AddField) and 0065_merge_* reconcile the two 0064_* heads. No locking or NOT-NULL-without-default risk.

New/updated tests cover the notes field (admin ordering, search, API create/list/patch), the cert-template type-mutation lock, the certificate-details admin rendering, and the WHOIS Selenium error helper.

No critical bugs or security vulnerabilities in the changed code.

Files Reviewed (incremental — feature scope)
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/static/config/js/switcher.js
  • openwisp_controller/config/static/config/css/admin.css
  • openwisp_controller/config/templates/admin/config/device/change_form.html
  • openwisp_controller/config/templates/admin/config/device_certificates_table.html
  • openwisp_controller/config/migrations/0064_template_notes.py
  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/whois/tests/tests.py

Previous review (commit b9a9d2a)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since 52c7544)

The new commit (b9a9d2a) fixes failing tests introduced by the prior certificate-details work. Only test plumbing and whitespace change:

  • openwisp_controller/config/admin.py — Removes a single blank line inside certificate_details; no functional change.
  • openwisp_controller/config/tests/test_admin.py — In _verify_template_queries, adds ContentType.objects.get_for_model(Device) before assertNumQueries to warm the content-type cache and make the query count deterministic, and bumps expected_count from 23 to 25 to account for the new certificate_details readonly queries. Also bumps _deactivated_device_expected_readonly_fields from 22 to 23 to reflect the new readonly field on ConfigInline. Device and ContentType are already imported; no duplicate imports introduced.

No production request handlers, serializers, migrations, tenant-filtering, or security-relevant code paths are modified in this delta. No new critical bugs or security vulnerabilities.

Files Reviewed (incremental, 2 files)
  • openwisp_controller/config/admin.py - 0 issues
  • openwisp_controller/config/tests/test_admin.py - 0 issues

Previous review (commit 52c7544)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since b486eb5)

The new commit (52c7544) adds a read-only "certificate details" panel to the device admin, surfacing the X.509 certificates generated for a device's certificate templates.

  • openwisp_controller/config/admin.py — Registers a certificate_details readonly field on ConfigInline that queries DeviceCertificate rows for the inline's config (with select_related on cert and template) and renders admin/config/device_certificates_table.html. The method guards the obj is None/new-instance case and the dc.cert is-null case. HTML output is produced via render_to_string, which returns a SafeString (same as the existing system_context readonly method), so the table renders correctly without escaping. Admin links are built with reverse, and template variables are auto-escaped by the template engine, so no XSS vector is introduced. A new regression/feature test is included.
  • admin/config/device_certificates_table.html — New template rendering the certificate table; uses i18n tags and auto-escaped variables. Safe.
  • tests/.../test_admin.py — Adds test_device_certificate_details_visible_in_admin, which creates a cert template + device config and asserts the template name, common name, cert UUID, status, and "View Certificate" link appear on the device change page. Coverage for the new feature is present.

No production request handlers, serializers, migrations, or tenant-filtering code paths are changed in this delta. No new critical bugs or security vulnerabilities were introduced.

Files Reviewed (incremental, 3 files)
  • openwisp_controller/config/admin.py - 0 issues
  • openwisp_controller/config/templates/admin/config/device_certificates_table.html - 0 issues
  • openwisp_controller/config/tests/test_admin.py - 0 issues

Previous review (commit b486eb5)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since b52fe63)

The new commits are low-risk follow-ups:

  • docs/user/certificate-templates.rst — Fixes a broken image URL: the certificate-template screenshot path dropped the stale docs/docs/1.4/ segment and now points to docs/certificate-templates/certificate-template.png, matching the actual branch path. No behavior change.
  • tests/openwisp2/sample_users/migrations/0004_default_groups.py — Adds a RunPython step calling add_api_key_permissions_to_admins (reverse = noop) to seed API-key permissions for the admin group in the test sample app. Test scaffolding only.
  • tests/openwisp2/sample_users/migrations/0005_user_expiration_date_user_user_active_expiry_idx.py — Inserts a CreateModel for the ApiKey proxy model (bases=("authtoken.token",)) so the sample_users app ships a migration matching the proxy model declared in models.py. The proxy is created with empty indexes/constraints, consistent with a no-schema proxy.
  • tests/openwisp2/sample_users/models.py — Imports ApiKey from openwisp_users.base.models so the proxy model is registered with the app. Standard Django pattern for proxy models living in a base/swappable package.

All changes are confined to the test sample_users app and a docs image link; no production code paths, request handlers, serializers, or migrations under openwisp_controller/ are touched in this delta. No new critical bugs or security vulnerabilities were introduced.

Files Reviewed (incremental)
  • docs/user/certificate-templates.rst - 0 issues
  • tests/openwisp2/sample_users/migrations/0004_default_groups.py - 0 issues
  • tests/openwisp2/sample_users/migrations/0005_user_expiration_date_user_user_active_expiry_idx.py - 0 issues
  • tests/openwisp2/sample_users/models.py - 0 issues

Previous review (commit b52fe63)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since fb54051)

The new commit [fix] Added missing organizational_unit_name is a focused one-line bug fix: the blueprint cert's organizational_unit_name was not being copied into the generated DeviceCertificate cert in copy_from (openwisp_controller/config/base/device_certificate.py:121). It is now included alongside the other copied fields.

A corresponding regression test, test_cert_generation_copies_blueprint_organizational_unit_name, is added in test_template.py. It creates a cert template bound to a blueprint cert with organizational_unit_name="Network Operations", provisions a device, and asserts the generated cert carries the same organizational_unit_name.

No new critical bugs or security vulnerabilities were introduced.

Files Reviewed (incremental)
  • openwisp_controller/config/base/device_certificate.py - 0 issues
  • openwisp_controller/config/tests/test_template.py - 0 issues

Previous review (commit fb54051)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since 8c32282)

The new commit [tests] Improved coverage adds focused regression/coverage tests only — no production code changes. The added tests in test_device.py and test_template.py correctly reference existing symbols and exercise genuine code paths:

  • test_device_certificate_str_pending_generation — verifies the Pending Generation string for a DeviceCertificate with cert=None.
  • test_cert_used_as_blueprint_blocked — confirms a blueprint cert already bound to a device cannot be reassigned via DeviceCertificate.full_clean() (validation error on cert).
  • test_device_certificate_auto_x509_early_return — covers the early-return branch of _auto_x509() when a cert already exists.
  • test_device_certificate_common_name_format_fallback — exercises the common-name fallback format when device name equals mac address.
  • test_device_certificate_post_delete_object_does_not_exist — covers the ObjectDoesNotExist branch of post_delete.
  • test_regeneration_task_setting_disabled / test_regeneration_task_device_does_not_exist — cover the two early-return paths of regenerate_device_certificates_task.
  • test_regeneration_task_expected_cert_id_mismatch — verifies certs are not regenerated when expected_cert_ids do not match.
  • test_regeneration_task_notification_failure — verifies the notification failure is logged via logger.warning and does not crash the task.
  • test_auto_add_template_to_existing_configs_task_timeout — covers the SoftTimeLimitExceeded error path in the auto-add task.
  • test_get_value_for_comparison — covers the OrderedDict-to-dict normalization helper.
  • test_cert_template_config_none — verifies config=None is coerced to {} for cert templates.
  • test_validate_cert_template_changes_deleted_instance — confirms a stale (deleted) instance bypasses active-template validation cleanly.

No new critical bugs or security vulnerabilities were introduced.

Files Reviewed (incremental)
  • openwisp_controller/config/tests/test_device.py - 0 issues
  • openwisp_controller/config/tests/test_template.py - 0 issues
Carried-forward from prior review

All previously reviewed production files and their findings carry forward unchanged. Prior outstanding notes (cert-template validation regression test and template_ui.js browser coverage) were already resolved in earlier cycles.

Previous review (commit 8c32282)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since a819721)

The new commit [chores] Minor improvements is a focused, low-risk follow-up:

  • handlers.py — Real bug fix for partial saves: detect_hardware_drift now only treats name/mac_address as changed when those fields were actually saved (via the new _hardware_field_was_saved helper). Previously, dirty in-memory values for fields not in update_fields could spuriously trigger cert regeneration. A dedicated regression test test_hardware_drift_partial_save_ignores_dirty_memory is added (passes with the fix, would fail without it).
  • api/serializers.py — On device organization change, DeviceCertificate rows for the config are now deleted before clearing templates. This prevents stale certificates issued under the previous org's CA from lingering (cross-tenant hygiene). Deletion fires the existing post_delete receiver which revokes the underlying x509 certs, consistent with the normal unassign path.
  • admin.py — Removed the now-nonexistent template_ui.js from BaseConfigAdmin.Media.js (the file is no longer shipped under static/config/js/), cleaning up a dangling reference. This also resolves the prior outstanding note about browser coverage for template_ui.js.
  • Teststest_device.py hardware-drift tests now build a real CA + cert template (so the signal path is genuinely exercised) and use captureOnCommitCallbacks(execute=True); test_selenium.py swaps the raw Ca.objects.create for the shared _create_ca helper with a common_name; test_template.py makes the cert=None scenario explicit (auto_cert=False + assertIsNone).

No new critical bugs or security vulnerabilities were introduced by this commit.

Files Reviewed (incremental)
  • openwisp_controller/config/admin.py - 0 issues
  • openwisp_controller/config/api/serializers.py - 0 issues
  • openwisp_controller/config/handlers.py - 0 issues
  • openwisp_controller/config/tests/test_device.py - 0 issues
  • openwisp_controller/config/tests/test_selenium.py - 0 issues
  • openwisp_controller/config/tests/test_template.py - 0 issues
  • openwisp_controller/config/base/device_certificate.py (context only) - 0 issues
Carried-forward from prior review

All previously reviewed files and their findings carry forward unchanged. The prior outstanding note (missing regression test for cert-template validation in template.py) was already resolved in the previous cycle by test_active_mutation_blocked, and the template_ui.js browser-coverage note is now resolved by this commit removing the file from the admin media bundle.

Previous review (commit a819721)

Status: No Issues Found | Recommendation: Merge

Incremental Review (since 298572f)

The new commits add a large body of work: certificate regeneration on hardware drift (#1359), deactivated-device enforcement across config/connection/WHOIS/geo, pagination consolidation to OpenWispPagination, defensive data-migration fixes for the JSONField change (#1061), and a DeviceConnection view rename with backward-compatible aliases.

The previously flagged WARNING (missing regression test for cert-template validation in template.py) is now resolvedtest_active_mutation_blocked covers CA/blueprint mutation and type-change-away-from-cert on active templates.

Review highlights:

  • regenerate_device_certificates_task is idempotent (locks rows, skips certs whose cert_id already drifted from the expected map, revokes old + mints new).
  • _sync_device_certificates post_clear path correctly defers orphan deletion via transaction.on_commit so reorders don't revoke certs.
  • _save_without_resurrecting guards against connection/command resurrection after concurrent deletion.
  • Migration 0061 adds nullable checksum_db then batched backfill (chunked .iterator() + bulk_update) — no locking DDL / unsafe NOT NULL.
  • Historical data migrations gain resolve_config() string-tolerant parsing without changing their one-shot semantics.
  • View rename keeps DeviceConnenctionListCreateView / deviceconnection_details_view aliases (TODO remove in 1.4) and wires URLs to the new names.

Extensive new tests accompany each change and certificate-templates.rst + settings.rst docs were added/updated.

Files Reviewed (incremental)
  • openwisp_controller/config/api/serializers.py - 0 issues
  • openwisp_controller/config/api/views.py - 0 issues
  • openwisp_controller/config/base/config.py - 0 issues
  • openwisp_controller/config/base/device.py - 0 issues
  • openwisp_controller/config/base/device_certificate.py - 0 issues
  • openwisp_controller/config/base/template.py - 0 issues (prior warning resolved)
  • openwisp_controller/config/handlers.py - 0 issues
  • openwisp_controller/config/tasks.py - 0 issues
  • openwisp_controller/config/controller/views.py - 0 issues
  • openwisp_controller/config/admin.py - 0 issues
  • openwisp_controller/config/settings.py - 0 issues
  • openwisp_controller/config/migrations/* - 0 issues
  • openwisp_controller/config/static/config/js/* - 0 issues
  • openwisp_controller/config/whois/* - 0 issues
  • openwisp_controller/connection/base/models.py - 0 issues
  • openwisp_controller/connection/api/views.py - 0 issues
  • openwisp_controller/connection/tasks.py - 0 issues
  • openwisp_controller/geo/* - 0 issues
  • openwisp_controller/pki/api/views.py - 0 issues

Previous review (commit 298572f)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
openwisp_controller/config/base/template.py 301 The certificate-template validation change still lacks a targeted regression test
Other Observations (not in diff)

Previous findings on openwisp_controller/config/api/serializers.py and openwisp_controller/config/static/config/js/template_ui.js are resolved in this revision.

No additional issues in unchanged code.

Files Reviewed (5 files)
  • openwisp_controller/config/api/serializers.py - 0 issues
  • openwisp_controller/config/base/template.py - 1 issue
  • openwisp_controller/config/static/config/js/template_ui.js - 0 issues
  • openwisp_controller/config/tests/test_selenium.py - 0 issues
  • openwisp_controller/pki/tests/test_api.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 142.5K · Output: 36.6K · Cached: 3.1M

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/openwisp/openwisp-controller/issues/comments/4548211157","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/openwisp/openwisp-controller/pull/1378?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: ASSERTIVE\n> \n> **Plan**: Pro\n> \n> **Run ID**: `33bb61f8-c083-446e-8e45-44d753e7ff7b`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between dc55622dfd09741ac51aad38afaaa206714ca875 and 25f1a213225299ecb5dc0ae4960630f68f8d8480.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (3)</summary>\n> \n> * `openwisp_controller/config/base/template.py`\n> * `openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py`\n> * `tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py`\n> \n> </details>\n> \n> ```ascii\n>  __________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________\n> < I've seen things you people wouldn't believe. Inefficient loops on fire off the shoulder of Orion. I've observed algorithms unfold in the dark near the Tannhäuser Gate, and watched data structures dissolve into the void of garbage collection. All those moments will be lost in my transient GPU cache, like tears in rain. >\n>  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-4548221491\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-4548221491\"} -->   Commit unit tests in branch `issues/1356-extend-abstract-template`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=openwisp/openwisp-controller&utm_content=1378)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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_controller/config/base/template.py`:
- Around line 265-267: The help text for the auto_cert field is out of date (it
still says it's only valid for VPN templates) — update the auto_cert field's
help/verbose/help_text in the Template definition in
openwisp_controller/config/base/template.py so it matches the new behavior
(auto_cert is allowed when type == "cert" as well as when type == "vpn"); locate
the auto_cert attribute (and any admin/API serializer or form label/help_text
referencing it) and change the message to something like "Valid for 'vpn' and
'cert' template types" or equivalent clear wording that includes both types.
🪄 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: 33bb61f8-c083-446e-8e45-44d753e7ff7b

📥 Commits

Reviewing files that changed from the base of the PR and between dc55622 and 25f1a21.

📒 Files selected for processing (3)
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.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_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
🧠 Learnings (4)
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
🔇 Additional comments (3)
openwisp_controller/config/base/template.py (1)

25-29: LGTM!

Also applies to: 62-83, 251-253, 271-312

openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py (1)

1-59: LGTM!

tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py (1)

1-58: LGTM!

Comment thread openwisp_controller/config/base/template.py Outdated
@openwisp-companion

Copy link
Copy Markdown
{
  "failures": [
    {
      "type": "Build/Infrastructure/Other",
      "message": "Migrations check failed! Models' changes not migrated, please run './manage.py makemigrations' to solve the issue!",
      "details": "The CI environment detected that the database migrations are out of sync. This is indicated by the error message 'Migrations check failed! Models' changes not migrated, please run './manage.py makemigrations' to solve the issue!' and the mention of a specific migration file '0005_user_expiration_date_user_user_active_expiry_idx.py'."
    }
  ]
}

CI Failure Analysis

Hello @stktyagi,
(Analysis for commit 25f1a21)

  • Migrations Out of Sync: The CI build failed because the database migrations are not up-to-date. This is indicated by the error message "Migrations check failed! Models' changes not migrated, please run './manage.py makemigrations' to solve the issue!".

Fix: Run ./manage.py makemigrations to generate the necessary migration files and then commit them to the repository.

stktyagi added 2 commits May 27, 2026 01:58
Updated previous tests and added new tests for implemetation.

Fixes #1356
Fixed help text for auto cert and updated migration files
@openwisp-companion

Copy link
Copy Markdown

Migrations Check Failed

Hello @stktyagi,
(Analysis for commit a795e09)

The CI failed because there are unapplied database migrations. This means that the database schema is out of sync with the current state of the code.

Fix:
Run the following command to apply the pending migrations:

./manage.py makemigrations

@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).

@stktyagi

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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_controller/config/base/template.py`:
- Around line 252-253: The validation of certificate relations (calls to
self._validate_org_relation("ca") and
self._validate_org_relation("blueprint_cert")) is running unconditionally and
can fail on stale relations when the object is being switched away from the
"cert" type; restrict these validations to the cert branch so they only run when
the template's type is "cert" (e.g., wrap or move the
self._validate_org_relation(...) calls inside the same conditional that handles
the "cert" branch or after the type check that preserves/clears ca and
blueprint_cert), ensuring they do not run when the code path clears those fields
(see the branch that clears ca and blueprint_cert).
- Around line 304-305: The current check "if not self.config" coerces any falsy
value ([], "", False) into {}, bypassing BaseConfig.clean() validation; change
the condition to only handle missing configs by checking "if self.config is
None" (or equivalent explicit None check) so only absent configs are replaced
with {} and invalid/falsy payloads are left intact for
full_clean()/BaseConfig.clean() to reject.
🪄 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: d3d93328-e58d-41dd-a374-dffebd6d1e38

📥 Commits

Reviewing files that changed from the base of the PR and between dc55622 and b946d26.

📒 Files selected for processing (5)
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/pki/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_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). (11)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.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:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
🧠 Learnings (4)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
🔇 Additional comments (4)
openwisp_controller/pki/tests/test_api.py (1)

155-155: LGTM!

Also applies to: 275-275

openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py (1)

12-16: LGTM!

Also applies to: 19-44, 45-58, 59-74

tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py (1)

11-14: LGTM!

Also applies to: 17-42, 43-56, 57-72

openwisp_controller/config/base/template.py (1)

25-29: LGTM!

Also applies to: 62-83, 119-120

Comment thread openwisp_controller/config/base/template.py Outdated
Comment thread openwisp_controller/config/base/template.py Outdated
@openwisp-companion

Copy link
Copy Markdown

Migrations Check Failed

Hello @stktyagi,
(Analysis for commit b946d26)

The CI failed because there are unapplied database migrations.

Failure: Migrations check failed! Models' changes not migrated, please run './manage.py makemigrations' to solve the issue!

Fix:
Run the following command to generate the missing migrations:

./manage.py makemigrations

stktyagi and others added 2 commits May 27, 2026 09:34
Validate cert relations only inside the cert branch and Only coerce missing cert configs, not every falsy value.

Fixes #1356
Comment thread openwisp_controller/config/base/template.py
Added test for the validation branch that now skips ca / blueprint_cert checks for non-cert templates

Fixes #1356
@coveralls

coveralls commented May 27, 2026

Copy link
Copy Markdown

Coverage Status

Coverage is 98.404%issues/1356-extend-abstract-template into gsoc26-x509-certificate-generator-templates. No base build found for gsoc26-x509-certificate-generator-templates.

Fixed line too long flake error

Fixes #1356
…1377

Implemented the DeviceCertificate M2M through-model to act as a strict relational bridge between Config, Template, and django_x509.Cert.

Fixes #1377
@stktyagi stktyagi added the gsoc Part of a Google Summer of Code project label May 28, 2026
Updated test by joining the list of strings into one sentence.

Fixes #1377
@stktyagi stktyagi changed the title [feature] Extended Template model for standalone X.509 certificates #1356 [feature] Introduced standalone certificate templates and device bindings May 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp_controller/config/apps.py (1)

93-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document why manage_device_certs must run before templates_changed.

The comment above manage_vpn_clients explains VPN clients must be created or removed before self.config_model.templates_changed is evaluated, because the VpnClient context can influence the configuration checksum. The same rationale applies to manage_device_certs (its DeviceCertificate rows feed get_cert_context(), which affects the checksum), and it is correctly positioned before templates_changed — but there's no comment explaining why, unlike its sibling. Without it, a future refactor could silently reorder these connects and break checksum correctness.

📝 Suggested comment
+        # DeviceCertificate rows must be created or removed **before**
+        # self.config_model.templates_changed is evaluated, because the
+        # cert context can influence the configuration checksum.
         m2m_changed.connect(
             self.config_model.manage_device_certs,
             sender=self.config_model.templates.through,
             dispatch_uid="config.manage_device_certs",
         )
🤖 Prompt for 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.

In `@openwisp_controller/config/apps.py` around lines 93 - 102, The
`m2m_changed.connect` for `manage_device_certs` needs an explanatory comment
like the nearby `manage_vpn_clients` block, since `DeviceCertificate` data is
used by `get_cert_context()` and must be updated before `templates_changed` runs
to keep the checksum correct. Add a brief note near the `manage_device_certs`
connection in `config_model` setup explaining that it must execute before
`templates_changed`, so future refactors do not reorder it and break checksum
generation.
🤖 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_controller/config/admin.py`:
- Around line 967-1011: The certificate-details queryset in
_add_certificate_details needs an explicit, deterministic order before slicing,
since the current [:51] truncation and has_more logic depend on stable row
ordering. Update the DeviceCertificate.objects.filter(...).select_related(...)
queryset to order by the same field used in Config.get_cert_context(),
specifically created, so the first 50 shown certificates are consistent across
requests and the “more” indicator is accurate.

In `@openwisp_controller/config/base/device_certificate.py`:
- Around line 12-13: Update the django-x509 dependency pin to a release that
supports custom oid extensions, since the current 1.4 branch tarball does not
handle the MAC_ADDRESS_OID and DEVICE_UUID_OID values used in
device_certificate.py. Adjust the dependency in requirements.txt to a version
with custom OID support, then verify the device certificate generation path
still works with those extension identifiers.

In `@openwisp_controller/config/base/template.py`:
- Around line 257-301: Add a regression test for the type-mutation branch in
_validate_cert_template_changes, since the existing deleted-instance coverage
does not hit current.type == "cert" and self.type != "cert". Create a test that
loads an active cert template, mutates its type away from "cert", and calls
clean() to assert a ValidationError is raised with an error on the type field.

In `@openwisp_controller/config/handlers.py`:
- Around line 98-107: The “active auto cert” queryset logic is duplicated in the
device certificate handling code and in the regeneration task, so extract the
shared filter into a single reusable API on DeviceCertificate. Add a
manager/classmethod such as DeviceCertificate.active_auto_certs_for(device) or
DeviceCertificate.objects.active_auto_certs_for(device) that encapsulates
config__device, auto_cert=True, cert__revoked=False, and template__type="cert",
then update both handlers.py and regenerate_device_certificates_task in tasks.py
to call that helper instead of repeating the filter inline.

In `@openwisp_controller/config/static/config/js/switcher.js`:
- Around line 47-63: The switcher logic contains a redundant show/hide branch
for auto_cert_field that duplicates the later conditional in the same handler.
Remove the առաջին duplicate if/else block and keep the unified val check that
both toggles auto_cert_field and updates auto_cert_label in switcher.js, using
the existing auto_cert_field and auto_cert_label symbols.

In `@openwisp_controller/config/tasks.py`:
- Around line 291-294: The exception handler in the regeneration notification
logic is redundant because `ImportError` is already covered by `Exception`.
Update the `except (ImportError, Exception) as e` block in the relevant task
function to catch only `Exception` while keeping the existing warning log
message with `device.name` and the exception details.

---

Outside diff comments:
In `@openwisp_controller/config/apps.py`:
- Around line 93-102: The `m2m_changed.connect` for `manage_device_certs` needs
an explanatory comment like the nearby `manage_vpn_clients` block, since
`DeviceCertificate` data is used by `get_cert_context()` and must be updated
before `templates_changed` runs to keep the checksum correct. Add a brief note
near the `manage_device_certs` connection in `config_model` setup explaining
that it must execute before `templates_changed`, so future refactors do not
reorder it and break checksum generation.
🪄 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: 08768b0a-73f9-4017-a7fc-c0025976cfcb

📥 Commits

Reviewing files that changed from the base of the PR and between 8b875e7 and 74a1e8f.

📒 Files selected for processing (33)
  • docs/developer/extending.rst
  • docs/index.rst
  • docs/user/certificate-templates.rst
  • docs/user/intro.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/templates.rst
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/models.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/config/static/config/js/switcher.js
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/templates/admin/config/device/change_form.html
  • openwisp_controller/config/templates/admin/config/device_certificates_table.html
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/pki/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/sample_config/models.py
  • tests/openwisp2/settings.py
📜 Review details
⚠️ CI failures not shown inline (11)

GitHub Actions: OpenWISP Controller CI Build / 7_Python==3.12 django~=4.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting propcache>=0.2.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (16 kB)
 Requirement already satisfied: typing_extensions>=4.4 in /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (4.16.0)
 Collecting yarl<2.0,>=1.17.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (94 kB)
 Collecting geographiclib<3,>=1.52 (from geopy~=2.4.1->django-loci @ https://github.com/openwisp/django-loci/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached geographiclib-2.1-py3-none-any.whl.metadata (1.6 kB)
 Requirement already satisfied: MarkupSafe>=2.0 in /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages (from jinja2~=3.1.6->netjsonconfig @ https://github.com/openwisp/netjsonconfig/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (3.0.3)
 Collecting jsonschema-specifications>=2023.03.6 (from jsonschema~=4.26.0->netjsonconfig @ https://github.com/openwisp/netjsonconfig/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached jsonschema_specifications-2025.9.1-py3-none-any.whl.metadata (2.9 kB)
 Collecting referencing>=0.28.4 (from jsonschema~=4.26.0->netjsonconfig @ https://github.com/openwisp/netjsonconfig/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached referencing-0.37.0-py3-none-any.whl.metadata (2.8 kB)
 Collecting rpds-py>=0.25.0 (from jsonschema~=4.26.0->netjsonconfig @ https://github.com/openwisp/netjsonconfig/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cac...

GitHub Actions: OpenWISP Controller CI Build / 1_Python==3.11 django~=5.1.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

x_2_28_x86_64.whl.metadata (8.3 kB)
 Collecting maxminddb<4.0.0,>=3.0.0 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached maxminddb-3.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Downloading aiohappyeyeballs-2.7.1-py3-none-any.whl.metadata (5.9 kB)
 Collecting aiosignal>=1.4.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB)
 Requirement already satisfied: attrs>=17.3.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (26.1.0)
 Collecting frozenlist>=1.1.1 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (20 kB)
 Collecting multidict<7.0,>=4.5 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting propcache>=0.2.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (16 kB)
 Requirement already satisfied: typing_extensions>=4.4 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (4.16.0)
 Collecting yarl<2.0,>=1.17.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (94 kB)
 Co...

GitHub Actions: OpenWISP Controller CI Build / 8_Python==3.10 django~=5.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

41021-py3-none-any.whl.metadata (5.5 kB)
 Collecting tablib>=3.7.0 (from django-import-export~=4.4.1->openwisp-controller==1.3a0)
   Using cached tablib-3.9.0-py3-none-any.whl.metadata (3.8 kB)
 Requirement already satisfied: rjsmin<2.0.0,>=1.2.0 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from django-minify-compress-staticfiles~=1.1.1->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (1.2.5)
 Requirement already satisfied: rcssmin<2.0.0,>=1.1.0 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from django-minify-compress-staticfiles~=1.1.1->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (1.2.2)
 Requirement already satisfied: brotli<2.0.0,>=1.2.0 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from django-minify-compress-staticfiles~=1.1.1->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (1.2.0)
 Collecting inflection>=0.3.1 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached inflection-0.5.1-py2.py3-none-any.whl.metadata (1.7 kB)
 Collecting pytz>=2021.1 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/op...

GitHub Actions: OpenWISP Controller CI Build / 9_Python==3.13 django~=5.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

nwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached pytz-2026.2-py2.py3-none-any.whl.metadata (22 kB)
 Requirement already satisfied: pyyaml>=5.1 in /opt/hostedtoolcache/Python/3.13.14/x64/lib/python3.13/site-packages (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (6.0.3)
 Collecting uritemplate>=3.0.0 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB)
 Collecting aiohttp<4.0.0,>=3.6.2 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.3 kB)
 Collecting maxminddb<4.0.0,>=3.0.0 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached maxminddb-3.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Downloading aiohappyeyeballs-2.7.1-py3-none-any.whl.metadata (5.9 kB)
 Collecting aiosignal>=1.4.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB)
 Requirement already satisfied: attrs>=17.3.0 in /opt/hostedtoolcache/Python/3.13.14/x64/lib/python3.13/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (26.1...

GitHub Actions: OpenWISP Controller CI Build / 11_Python==3.13 django~=5.1.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

s/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB)
 Collecting aiohttp<4.0.0,>=3.6.2 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.3 kB)
 Collecting maxminddb<4.0.0,>=3.0.0 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached maxminddb-3.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Downloading aiohappyeyeballs-2.7.1-py3-none-any.whl.metadata (5.9 kB)
 Collecting aiosignal>=1.4.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB)
 Requirement already satisfied: attrs>=17.3.0 in /opt/hostedtoolcache/Python/3.13.14/x64/lib/python3.13/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (26.1.0)
 Collecting frozenlist>=1.1.1 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (20 kB)
 Collecting multidict<7.0,>=4.5 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting propcache>=0.2.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (16 kB)
 Collecting yarl<2.0,>=1.17.0 (...

GitHub Actions: OpenWISP Controller CI Build / 3_Python==3.11 django~=4.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

2.0->openwisp-controller==1.3a0)
   Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB)
 Requirement already satisfied: attrs>=17.3.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (26.1.0)
 Collecting frozenlist>=1.1.1 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (20 kB)
 Collecting multidict<7.0,>=4.5 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting propcache>=0.2.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (16 kB)
 Requirement already satisfied: typing_extensions>=4.4 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (4.16.0)
 Collecting yarl<2.0,>=1.17.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (94 kB)
 Collecting geographiclib<3,>=1.52 (from geopy~=2.4.1->django-loci @ https://github.com/openwisp/django-loci/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached geographiclib-2.1-py3-none-any.whl.metadata (1.6 kB)
 Requirement already satisfied: MarkupSafe>=2.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from jinja2~=3.1.6->netjsonconfig @ https://github.com/openwisp/netjsonconfig/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (3.0.3)
 Collecting jsonschema...

GitHub Actions: OpenWISP Controller CI Build / 10_Python==3.11 django~=5.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

f-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached inflection-0.5.1-py2.py3-none-any.whl.metadata (1.7 kB)
 Collecting pytz>=2021.1 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached pytz-2026.2-py2.py3-none-any.whl.metadata (22 kB)
 Requirement already satisfied: pyyaml>=5.1 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (6.0.3)
 Collecting uritemplate>=3.0.0 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB)
 Collecting aiohttp<4.0.0,>=3.6.2 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.3 kB)
 Collecting maxminddb<4.0.0,>=3.0.0 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached maxminddb-3.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Downloading aioha...

GitHub Actions: OpenWISP Controller CI Build / 6_Python==3.12 django~=5.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

1.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (6.0.3)
 Collecting uritemplate>=3.0.0 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB)
 Collecting aiohttp<4.0.0,>=3.6.2 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.3 kB)
 Collecting maxminddb<4.0.0,>=3.0.0 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached maxminddb-3.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Downloading aiohappyeyeballs-2.7.1-py3-none-any.whl.metadata (5.9 kB)
 Collecting aiosignal>=1.4.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB)
 Requirement already satisfied: attrs>=17.3.0 in /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (26.1.0)
 Collecting frozenlist>=1.1.1 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (20 kB)
 Collecting multidict<7.0,>=4.5 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached multidict-6.7.1-cp312...

GitHub Actions: OpenWISP Controller CI Build / 2_Python==3.10 django~=4.2.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB)
 Collecting aiohttp<4.0.0,>=3.6.2 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.3 kB)
 Collecting maxminddb<4.0.0,>=3.0.0 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached maxminddb-3.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting aiohappyeyeballs>=2.5.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Downloading aiohappyeyeballs-2.7.1-py3-none-any.whl.metadata (5.9 kB)
 Collecting aiosignal>=1.4.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiosignal-1.4.0-py3-none-any.whl.metadata (3.7 kB)
 Requirement already satisfied: async-timeout<6.0,>=4.0 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (5.0.1)
 Requirement already satisfied: attrs>=17.3.0 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0) (26.1.0)
 Collecting frozenlist>=1.1.1 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (20 kB)
 Collecting multidict<7.0,>=4.5 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (5.3 kB)
 Collecting propcache>=0.2.0 (from aiohttp<4.0.0,>=3.6.2->geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached propcache-0.5.2-cp310-cp3...

GitHub Actions: OpenWISP Controller CI Build / 5_Python==3.10 django~=5.1.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

1.2.2)
 Requirement already satisfied: brotli<2.0.0,>=1.2.0 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from django-minify-compress-staticfiles~=1.1.1->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (1.2.0)
 Collecting inflection>=0.3.1 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached inflection-0.5.1-py2.py3-none-any.whl.metadata (1.7 kB)
 Collecting pytz>=2021.1 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached pytz-2026.2-py2.py3-none-any.whl.metadata (22 kB)
 Requirement already satisfied: pyyaml>=5.1 in /opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/site-packages (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0) (6.0.3)
 Collecting uritemplate>=3.0.0 (from drf-yasg<1.22.0,>=1.21.14->openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/1.3.tar.gz->openwisp-controller==1.3a0)
   Using cached uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB)
 Collecting aiohttp<4.0.0,>=3.6.2 (from geoip2<6.0.0,>=5.2.0->openwisp-controller==1.3a0)
   Using cached aiohttp-3.14.1-cp3...

GitHub Actions: OpenWISP Controller CI Build / 4_Python==3.12 django~=5.1.0.txt: [feature] Introduced standalone certificate templates and device bindings

Conclusion: failure

View job details

oller.pki.tests.test_api.TestPkiApi.test_cert_patch_api)�[0m
 �[33;1m  (0.47s)�[0m test_cert_patch_api
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_cert_post_api)�[0m
 �[33;1m  (0.46s)�[0m test_cert_post_api
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_cert_post_with_date_none)�[0m
 �[33;1m  (0.50s)�[0m test_cert_post_with_date_none
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_cert_post_with_extensions_field)�[0m
 �[33;1m  (0.50s)�[0m test_cert_post_with_extensions_field
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_cert_put_api)�[0m
 �[33;1m  (0.40s)�[0m test_cert_put_api
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_crl_download_api)�[0m
 �[33;1m  (0.43s)�[0m test_crl_download_api
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_import_cert_post_api)�[0m
 �[33;1m  (0.41s)�[0m test_import_cert_post_api
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_post_cert_renew_api)�[0m
 �[33;1m  (0.52s)�[0m test_post_cert_renew_api
 �[33;1m(openwisp_controller.pki.tests.test_api.TestPkiApi.test_post_cert_revoke_api)�[0m
 �[33;1m  (0.47s)�[0m test_post_cert_revoke_api
 �[33;1m(openwisp_controller.pki.tests.test_models.TestModels.test_ca_creation_with_org)�[0m
 �[33;1m  (0.30s)�[0m test_ca_creation_with_org
 �[33;1m(openwisp_controller.pki.tests.test_models.TestModels.test_ca_creation_without_org)�[0m
 �[33;1m  (0.30s)�[0m test_ca_creation_without_org
 �[33;1m(openwisp_controller.pki.tests.test_models.TestModels.test_cert_and_ca_different_organization)�[0m
 �[33;1m  (0.32s)�[0m test_cert_and_ca_different_organization
 �[33;1m(openwisp_controller.pki.tests.test_models.TestModels.test_cert_creation)�[0m
 �[33;1m  (0.43s)�[0m test_cert_creation
 �[33;1m(openwisp_controller.pki.tests.test_models.TestModels.test_crl_view)�[0m
 �[33;1m  (0.36s)�[0m test_crl_view
 �[33;1m(openwisp_controller.pki.tests.test_models.TestModels.test_unique_together_org_none)�[0m
 �...
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/apps.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/models.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.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:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/config/templates/admin/config/device/change_form.html
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • openwisp_controller/config/templates/admin/config/device_certificates_table.html
  • tests/openwisp2/settings.py
  • openwisp_controller/config/apps.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/models.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
🧠 Learnings (9)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/models.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/models.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/apps.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/models.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/settings.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/settings.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
🪛 ast-grep (0.44.0)
openwisp_controller/config/tests/test_selenium.py

[warning] 25-25: 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("openwisp_notifications", "Notification")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py

[info] 106-106: use help_text to document model columns
Context: models.CharField(blank=True, max_length=64, null=True)
Note: [CWE-710] Improper Adherence to Coding Standards.

(model-help-text)

openwisp_controller/config/handlers.py

[warning] 98-98: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/tasks.py

[warning] 232-232: 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("config", "Device")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 233-233: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/device_certificate.py

[warning] 41-41: 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("config", "Template")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/template.py

[warning] 40-40: 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("django_x509", "Cert")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 41-41: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 275-275: 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("config", "Config")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 338-338: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/tests/test_device.py

[warning] 30-30: 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("django_x509", "Cert")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 31-31: 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("django_x509", "Ca")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 32-32: 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("config", "OrganizationConfigSettings")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

🪛 HTMLHint (1.9.2)
openwisp_controller/config/templates/admin/config/device_certificates_table.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

Comment thread openwisp_controller/config/admin.py
Comment thread openwisp_controller/config/base/device_certificate.py
Comment thread openwisp_controller/config/base/template.py
Comment thread openwisp_controller/config/handlers.py
Comment thread openwisp_controller/config/static/config/js/switcher.js Outdated
Comment thread openwisp_controller/config/tasks.py Outdated
stktyagi added 2 commits July 7, 2026 19:43
Addressed suggested refactor and additions
Simplified it to except Exception as e: since ImportError is already a subclass of Exception
@openwisp-companion

Copy link
Copy Markdown

Migrations Check Failed

Hello @stktyagi,
(Analysis for commit 73b3283)

There is a failure in the migrations check. This indicates that the database
migrations have not been applied correctly.

Fix:
Run ./manage.py makemigrations to generate the necessary migration files and
then commit them.

@openwisp-companion

Copy link
Copy Markdown

Migrations Check Failed

Hello @stktyagi,
(Analysis for commit bd68792)

The CI failed because of a migrations check failure. This indicates that the database schema might be out of sync with the current code.

Fix:

Please run the following command to generate the necessary migration files:

./manage.py makemigrations

After running the command, commit the generated migration files and push again.

Comment thread openwisp_controller/config/handlers.py Outdated
try:
old_instance = sender.objects.only("name", "mac_address").get(pk=instance.pk)
instance._old_name = old_instance.name
instance._old_mac = old_instance.mac_address

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 can be achieved inside the model, without this extra logic which also adds more queries. See https://github.com/search?q=repo%3Aopenwisp%2Fopenwisp-controller%20_initial&type=code

@nemesifier

Copy link
Copy Markdown
Member

@coderabbitai any outstanding issue?

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get current unresolved review threads on PR 1378
gh api graphql -f query='
query {
  repository(owner: "openwisp", name: "openwisp-controller") {
    pullRequest(number: 1378) {
      reviewThreads(first: 100) {
        nodes {
          isResolved
          isOutdated
          comments(first: 1) {
            nodes {
              path
              body
              author { login }
              createdAt
            }
          }
        }
      }
    }
  }
}' | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {path: .comments.nodes[0].path, isOutdated, body: .comments.nodes[0].body[0:300], author: .comments.nodes[0].author.login}]'

Length of output: 12209


@nemesifier Here's the current state based on unresolved review threads (excluding ones marked outdated by GitHub, which were superseded by later commits):

🔴 Still appears open / needs attention

  1. openwisp_controller/config/base/device_certificate.py — OID extension format mismatch (blocker)
    _get_hardware_oid_extensions() builds extension dicts with oid, value, critical keys, but the installed django-x509 validates extensions expecting name, critical, value. If unaddressed, certificate generation would fail whenever these hardware extensions are attached. I don't see evidence in the latest diff that the key names were fixed — worth double-checking this before merge.

  2. openwisp_controller/config/base/config.py — through-model lifecycle consistency
    DeviceCertificate rows are created via the manage_device_certs m2m_changed handler, but paths like Config.add_default_templates(), AbstractTemplate._auto_add_to_existing_configs(), controller-registration auto-tagging, and direct config.templates.add() may not consistently trigger the same post_add logic, depending on how they invoke the M2M. Worth confirming these all route through templates.add()/.set() so the signal fires.

  3. openwisp_controller/config/base/config.py — if-else ladder in manage_device_certs needs an explanatory comment (requested by Aryamanz29).

  4. openwisp_controller/config/handlers.py — hardware-drift detection adds extra queries
    Suggestion to move the "did name/mac change" check into the model layer (e.g., via Django's _initial snapshot pattern) instead of the extra pre_save lookup query.

🟡 Likely already addressed, but threads not yet closed on GitHub (worth a quick `` pass once verified)

  • docs/index.rst / docs/user/intro.rst — certificate-templates page linked (already present per current diff).
  • docs/developer/extending.rstCONFIG_DEVICECERTIFICATE_MODEL entry added.
  • docs/user/settings.rstREGENERATE_CERTS_ON_HARDWARE_CHANGE documented and COMMON_NAME_FORMAT text updated.
  • docs/user/templates.rst — now links to certificate-templates.rst with a toctree entry in docs/index.rst.
  • config.py — standalone cert renewal now updates config status (certificate_updated, covered by test_standalone_cert_renewal_updates_config_status).
  • admin.py — device admin visibility for certificate details implemented (device_certificates_table.html + test_device_certificate_details_visible_in_admin).
  • device_certificate.pyorganizational_unit_name now cloned from blueprint (covered by test_cert_generation_copies_blueprint_organizational_unit_name).

🔵 Not yet confirmed either way

  • Swagger/DRF browsable API reflecting new ca/blueprint_cert fields — no direct test evidence found for this specific check.
  • Missing docs image (certificate-template.png 404) — needs a follow-up PR against the docs branch.

I'd recommend triggering a fresh @coderabbitai full review after confirming item #1 (OID extension keys) is fixed, since that's the one blocker with no clear resolution evidence yet.

✅ Action performed

Review finished.

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.

@stktyagi stktyagi changed the title [feature] Introduced standalone certificate templates and device bindings [feature] Added X.509 Certificate Generator Templates Jul 10, 2026
stktyagi and others added 2 commits July 10, 2026 20:01
@stktyagi

Copy link
Copy Markdown
Member Author

🔴 Still appears open / needs attention

  1. openwisp_controller/config/base/device_certificate.py — OID extension format mismatch (blocker)
    _get_hardware_oid_extensions() builds extension dicts with oid, value, critical keys, but the installed django-x509 validates extensions expecting name, critical, value. If unaddressed, certificate generation would fail whenever these hardware extensions are attached. I don't see evidence in the latest diff that the key names were fixed — worth double-checking this before merge.
  2. openwisp_controller/config/base/config.py — through-model lifecycle consistency
    DeviceCertificate rows are created via the manage_device_certs m2m_changed handler, but paths like Config.add_default_templates(), AbstractTemplate._auto_add_to_existing_configs(), controller-registration auto-tagging, and direct config.templates.add() may not consistently trigger the same post_add logic, depending on how they invoke the M2M. Worth confirming these all route through templates.add()/.set() so the signal fires.
  3. openwisp_controller/config/base/config.py — if-else ladder in manage_device_certs needs an explanatory comment (requested by Aryamanz29).
  4. openwisp_controller/config/handlers.py — hardware-drift detection adds extra queries
    Suggestion to move the "did name/mac change" check into the model layer (e.g., via Django's _initial snapshot pattern) instead of the extra pre_save lookup query.

@coderabbitai see below:

  1. device_certificate.py — OID extension format mismatch — Not a blocker. django-x509's _verify_extension_format() (base/models.py:594) explicitly accepts both "name" and "oid" as extension identifiers. When "oid" is present, the dict keys oid, value, critical are validated correctly (lines 601-619), and _add_extensions() (line 772) processes them as UnrecognizedExtension. The format in _get_hardware_oid_extensions() is exactly what's expected.

  2. config.py — through-model lifecycle consistency — All paths route through the standard Django M2M API (add()/remove()/clear()), which always fires m2m_changed:

  • add_default_templates() → self.templates.add() ✓
  • _auto_add_to_existing_configs() → config.templates.add(self) ✓
  • add_tagged_templates() → config.templates.add(*...) ✓
  • manage_group_templates() → self.templates.add(*...) ✓
  • deactivate() → self.templates.clear() ✓
    The instance._state.adding guard is not an issue — add_default_templates() runs after super().save() (line 640), so _state.adding is already False. No path bypasses the signal.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Device certificate extension construction ==='
ast-grep outline openwisp_controller/config/base/device_certificate.py --items all --match 'hardware|extension|build' || true
rg -n -A24 -B8 '_get_hardware_oid_extensions|_build_extensions|_build_cert' \
  openwisp_controller/config/base/device_certificate.py

echo '=== M2M signal registration and lifecycle implementation ==='
rg -n -A12 -B8 'm2m_changed|manage_device_certs|add_default_templates|add_tagged_templates|manage_group_templates|_auto_add_to_existing_configs|def deactivate' \
  openwisp_controller/config/base/config.py \
  openwisp_controller/config/base/template.py \
  openwisp_controller/config/apps.py

echo '=== Declared django-x509 dependency/source availability ==='
rg -n -i -C2 'django[-_]?x509|django_x509' pyproject.toml setup.cfg setup.py requirements*.txt 2>/dev/null || true
fd -HI '^models\.py$' . | while read -r f; do
  if rg -q '_verify_extension_format|def _add_extensions' "$f"; then
    echo "--- $f"
    rg -n -A34 -B5 '_verify_extension_format|def _add_extensions' "$f"
  fi
done

Length of output: 36574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== django-x509 1.4 extension contract (declared dependency branch) ==='
gh api 'repos/openwisp/django-x509/contents/django_x509/base/models.py?ref=1.4' \
  --jq '.content' | base64 --decode \
  | nl -ba \
  | sed -n '585,625p;760,795p'

echo '=== Certificate-template M2M call paths ==='
sed -n '219,260p' openwisp_controller/config/base/template.py
sed -n '1047,1085p' openwisp_controller/config/base/config.py

echo '=== Tagged-template assignment implementations ==='
rg -n -A24 -B5 'def add_tagged_templates|\.templates\.(add|remove|clear|set)\s*\(' \
  openwisp_controller/config --glob '*.py'

Length of output: 1054


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== django-x509 1.4 extension contract (declared dependency branch) ==='
gh api 'repos/openwisp/django-x509/contents/django_x509/base/models.py?ref=1.4' \
  --jq '.content' | base64 --decode \
  | awk 'NR >= 585 && NR <= 625 || NR >= 760 && NR <= 795 { printf "%6d  %s\n", NR, $0 }'

echo '=== Certificate-template M2M call paths ==='
sed -n '219,260p' openwisp_controller/config/base/template.py
sed -n '1047,1085p' openwisp_controller/config/base/config.py

echo '=== Tagged-template assignment implementations ==='
rg -n -A24 -B5 'def add_tagged_templates|\.templates\.(add|remove|clear|set)\s*\(' \
  openwisp_controller/config --glob '*.py'

Length of output: 233996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' /tmp/coderabbit-shell-logs/shell-output-d0UbqU

Length of output: 9892


@stktyagi You are correct; I verified both points against the current branch and django-x509’s declared 1.4 dependency source.

  1. OID extensions: django-x509 explicitly accepts oid as an alternative to name, validates the value and optional boolean critical fields, and constructs an x509.UnrecognizedExtension for such entries. The payload generated by _get_hardware_oid_extensions() is valid. I withdraw this concern.

  2. M2M lifecycle: manage_device_certs is registered on Config.templates.through. The cited flows use the standard manager API:

    • _auto_add_to_existing_configs()config.templates.add(self)
    • add_default_templates()self.templates.add(...)
    • manage_group_templates()self.templates.remove(...) / .add(...)
    • deactivate()self.templates.clear()

    Therefore, these paths emit m2m_changed; the _state.adding guard does not skip default-template handling because it runs after super().save(). I withdraw this concern as well.

The separate documentation/comment request for the manage_device_certs branches and the hardware-drift query optimization are independent non-blocking suggestions.

🐇🔎


✏️ Learnings added
Learnt from: stktyagi
URL: https://github.com/openwisp/openwisp-controller/pull/1378

Timestamp: 2026-07-10T14:43:25.266Z
Learning: In `openwisp_controller/config/base/device_certificate.py`, hardware extensions must use the django-x509 custom-extension form `{"oid": ..., "value": ..., "critical": ...}`. The django-x509 `1.4` branch explicitly supports `oid` as an extension identifier and creates an `x509.UnrecognizedExtension` from it.

Learnt from: stktyagi
URL: https://github.com/openwisp/openwisp-controller/pull/1378

Timestamp: 2026-07-10T14:43:25.266Z
Learning: In `openwisp_controller/config/base/config.py`, certificate-template lifecycle synchronization is driven by `AbstractConfig.manage_device_certs`, registered on `Config.templates.through` with Django’s `m2m_changed` signal. Standard paths including `AbstractConfig.add_default_templates`, `AbstractTemplate._auto_add_to_existing_configs`, `AbstractConfig.manage_group_templates`, and `AbstractConfig.deactivate` use the M2M manager API and therefore trigger this handler.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Track mac_address in _changed_checked_fields so the model's __init__
sets _initial_mac_address automatically, matching the existing
_initial_name pattern.
@stktyagi

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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_controller/config/tests/test_device.py`:
- Line 668: New hardware-drift tests create CAs without model validation. Add
the appropriate PKI/VPN test mixin to TestDevice so _create_ca() is available,
then replace every Ca.objects.create(name="test-ca", organization=org) in the
affected tests with self._create_ca(name="test-ca", organization=org),
preserving full_clean() validation.
🪄 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: ba76345b-91e8-4c0b-85b3-35231934acee

📥 Commits

Reviewing files that changed from the base of the PR and between 08a0821 and 375844d.

📒 Files selected for processing (34)
  • docs/developer/extending.rst
  • docs/index.rst
  • docs/user/certificate-templates.rst
  • docs/user/intro.rst
  • docs/user/rest-api.rst
  • docs/user/settings.rst
  • docs/user/templates.rst
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/models.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/config/static/config/js/switcher.js
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/templates/admin/config/device/change_form.html
  • openwisp_controller/config/templates/admin/config/device_certificates_table.html
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/pki/tests/test_api.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/sample_config/models.py
  • tests/openwisp2/settings.py
📜 Review details
⏰ Context from checks skipped due to timeout. (12)
  • GitHub Check: CodeRabbit / Review
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/models.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/admin.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/tests/test_device.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:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • openwisp_controller/config/templates/admin/config/device/change_form.html
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/models.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/templates/admin/config/device_certificates_table.html
  • openwisp_controller/config/settings.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/admin.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/tests/test_device.py
🧠 Learnings (9)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/models.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/models.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/migrations/0065_merge_20260702_1946.py
  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/models.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/admin.py
  • tests/openwisp2/sample_config/models.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/device_certificate.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • tests/openwisp2/sample_config/models.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py
  • tests/openwisp2/settings.py
  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • tests/openwisp2/sample_config/models.py
  • tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/config/tests/test_admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_selenium.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_template.py
  • openwisp_controller/config/tests/test_device.py
🪛 ast-grep (0.44.1)
openwisp_controller/config/handlers.py

[warning] 82-82: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py

[info] 106-106: use help_text to document model columns
Context: models.CharField(blank=True, max_length=64, null=True)
Note: [CWE-710] Improper Adherence to Coding Standards.

(model-help-text)

openwisp_controller/config/tasks.py

[warning] 232-232: 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("config", "Device")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 233-233: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/tests/test_selenium.py

[warning] 25-25: 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("openwisp_notifications", "Notification")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/template.py

[warning] 40-40: 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("django_x509", "Cert")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 41-41: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 275-275: 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("config", "Config")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 338-338: 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("config", "DeviceCertificate")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/device_certificate.py

[warning] 41-41: 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("config", "Template")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/tests/test_device.py

[warning] 30-30: 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("django_x509", "Cert")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 31-31: 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("django_x509", "Ca")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 32-32: 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("config", "OrganizationConfigSettings")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

🪛 HTMLHint (1.9.2)
openwisp_controller/config/templates/admin/config/device_certificates_table.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

🔇 Additional comments (35)
docs/developer/extending.rst (1)

345-345: LGTM!

docs/index.rst (1)

40-40: LGTM!

docs/user/certificate-templates.rst (1)

1-291: LGTM!

docs/user/intro.rst (1)

38-39: LGTM!

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

1114-1115: LGTM!

docs/user/settings.rst (1)

310-313: LGTM!

Also applies to: 333-350, 371-377

docs/user/templates.rst (1)

212-229: LGTM!

openwisp_controller/config/templates/admin/config/device_certificates_table.html (1)

1-49: LGTM!

openwisp_controller/config/base/template.py (1)

25-29: LGTM!

All previously flagged issues (NULL handling in get_unassigned_certs, revoked-cert rejection, active-template CA/blueprint/type mutation locks with regression test coverage, config is None vs falsy check) are present and correctly implemented in the current state of the file. No new issues found.

Also applies to: 40-51, 74-97, 134-137, 257-394

openwisp_controller/config/migrations/0064_template_blueprint_cert_template_ca_and_more.py (1)

1-153: LGTM!

Migration correctly mirrors the model definitions in template.py (blueprint_cert, ca, updated auto_cert help text, updated type choices) and correctly includes the "swappable": "CONFIG_DEVICECERTIFICATE_MODEL" option on the production DeviceCertificate model.

openwisp_controller/config/migrations/0065_merge_20260702_1946.py (1)

1-13: LGTM!

Trivial merge migration with no schema operations.

openwisp_controller/config/models.py (1)

5-5: LGTM!

The concrete DeviceCertificate model correctly subclasses AbstractDeviceCertificate and registers the swappable setting for config.DeviceCertificate.

Also applies to: 97-106

tests/openwisp2/sample_config/migrations/0010_template_blueprint_cert_template_ca_and_more.py (1)

1-151: LGTM!

The sample migration intentionally omits the swappable option (correct, since sample_config.DeviceCertificate is the concrete replacement, not the swappable base) and the additional details field follows the documented django-x509-style extension pattern for demonstrating custom model extension in the test app.

openwisp_controller/config/api/serializers.py (1)

20-20: LGTM!

All previously flagged concerns are addressed in the current state: cert_fields_provided is captured before ca/blueprint_cert normalization (avoiding the performance regression), model-instance comparisons (not PK vs instance mismatches) are used for the active-template mutation lock, the type field is included in the mutation lock, and the explicit DeviceCertificate cleanup in both _update_config and the organization-change path in DeviceDetailSerializer.update are confirmed intentional/necessary given manage_device_certs's post_clear no-op for active devices.

Also applies to: 42-43, 53-62, 79-87, 88-173, 324-326, 440-452

openwisp_controller/config/admin.py (1)

23-23: LGTM!

_add_certificate_details correctly orders the queryset by created before slicing (previously-flagged pagination-stability concern is fixed), and select_related("cert__ca", "template") avoids N+1 queries when building cert_data. I verified that adding "ca" and "blueprint_cert" to TemplateAdmin.autocomplete_fields is safe: django-x509's CaAdmin/CertAdmin inherit search_fields = ['name', 'serial_number', 'common_name'] from BaseAdmin and are registered via admin.site.register, so this will not trigger a Django admin system-check error (admin.E039/E040).

Also applies to: 64-64, 117-126, 928-929, 967-1013, 1117-1118, 1132-1141, 1154-1154

openwisp_controller/config/base/device_certificate.py (2)

41-42: False positive: not a Keras model load.

The static analysis hint about untrusted Keras model deserialization on load_model("config", "Template") is a false positive — this is swapper.load_model, used to resolve the swappable Template model class, unrelated to Keras/ML model loading.

Source: Linters/SAST tools


1-186: LGTM!

The remaining known items (django-x509 OID-extension version pin, upstream django-x509#228-blocked custom OID injection) are already tracked in prior discussion and explicitly deferred by the author; no new issues found in this revision.

tests/openwisp2/sample_config/migrations/0011_merge_20260702_1946.py (1)

1-14: LGTM!

tests/openwisp2/sample_config/models.py (1)

5-5: LGTM!

Also applies to: 99-107

tests/openwisp2/settings.py (1)

284-284: LGTM!

openwisp_controller/config/base/config.py (1)

66-72: LGTM!

certificate_updated now correctly resolves configs impacted through DeviceCertificate (not just vpnclient), and manage_device_certs now carries explanatory comments for each branch as previously requested. No new issues found in this revision.

Also applies to: 474-489, 502-548, 978-1004, 1032-1032

openwisp_controller/config/templates/admin/config/device/change_form.html (1)

13-28: LGTM!

openwisp_controller/config/static/config/js/switcher.js (1)

4-15: LGTM!

Also applies to: 42-62

openwisp_controller/config/apps.py (1)

72-72: LGTM!

Also applies to: 98-102, 124-128

openwisp_controller/config/tasks.py (1)

226-289: LGTM!

openwisp_controller/config/tests/test_config.py (1)

861-871: LGTM!

Based on learnings, query-count changes in assertNumQueries calls across this repo's test files are treated as routine maintenance as the feature set evolves, not as regressions requiring investigation.

openwisp_controller/config/tests/test_selenium.py (1)

25-26: LGTM!

Also applies to: 491-529, 530-561, 846-878

openwisp_controller/pki/tests/test_api.py (1)

155-155: LGTM!

Based on learnings, assertNumQueries count changes in this repo's test files are routine maintenance as features evolve and don't warrant inline explanatory comments or regression investigation.

Also applies to: 256-256, 267-278, 292-292

openwisp_controller/config/tests/test_device.py (1)

6-6: 🩺 Stability & Availability

Verify TransactionTestCase is still needed. Remove the import if no test in this file depends on it; otherwise point it at the test that uses it.

openwisp_controller/config/settings.py (1)

37-39: 📐 Maintainability & Code Quality | ⚡ Quick win

Confirm docs/user/settings.rst was updated for this new setting.

A prior reviewer noted that OPENWISP_CONTROLLER_REGENERATE_CERTS_ON_HARDWARE_CHANGE wasn't documented in docs/user/settings.rst, and that OPENWISP_CONTROLLER_COMMON_NAME_FORMAT's docs still only mention VPN client certificates even though standalone cert templates now use it too. No confirmation of a fix is visible in this batch (the docs layer isn't included here).

openwisp_controller/config/base/device.py (1)

33-39: LGTM!

Also applies to: 372-380

openwisp_controller/config/handlers.py (1)

48-95: LGTM!

openwisp_controller/config/tests/test_template.py (1)

973-1589: LGTM!

openwisp_controller/config/tests/test_admin.py (1)

1824-1857: LGTM!

Based on learnings, query-count assertions in _verify_template_queries are treated as volatile implementation details that don't need inline explanation, so the expected_count = 24 change needs no further comment.

Also applies to: 2389-2394

openwisp_controller/config/tests/test_api.py (1)

1338-1545: LGTM!

Comment thread openwisp_controller/config/tests/test_device.py Outdated
Used _create_ca()/full_clean() like the rest of the file does for Cert.
@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).

:target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/certificate-templates/certificate-template.png
:alt: Certificate Template admin form

:guilabel:`Certificate Authority` (required)

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.

@stktyagi are you testing the build of openwisp-docs with this document? If you haven't yet, please do, as it will be one of the next things I will start testing once I tested all the other aspects.

TYPE_CHOICES = (
("generic", _("Generic")),
("vpn", _("VPN-client")),
("cert", _("Certificate")),

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.

shouldn't we call this "Certificate generator" ?
I think "Certificate" here is too vague and bad for UX as it's confusing and I also think the same for the docs text which is not using the "Certificate generator" wording nor any equivalent meaning. We must indicate that these templates automate the creation of X509 certificates.

"by this template."
),
)

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.

avoid needless blank lines

if certs_regenerated > 0:
try:
message = _(
"Hardware drift detected on device {device_name}. "

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.

The notification text used here reads AI sloppy to me. Who says "hardware drift" in every day language? A user is going to read this and think "WTF"?



@shared_task(soft_time_limit=1200)
def regenerate_device_certificates_task(device_id, expected_cert_ids=None):

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 task seems too big to me, can you move this logic to the most suited Django model (eg AbstractDeviceCertificate)? A class should work fine.

("config", "0064_template_notes"),
]

operations = []

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.

@stktyagi what is this migration for?

@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.

Migration 0064 is not conflicting with config.0064_template_notes.

Due to the important changes done in #1418, I think it's time to sync your work with the latest master.

@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.

Same error message for both fields?

Image

Please make the error message specialized for each field.

@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.

Image
  • Certificate details > X.509 Certs
  • Eliminate "Certificates" heading, redundant
  • From: I am not sure this is clear, I'd call it "Template" and add a help icon (we should have one) next to it and wrap both (template word and help icon) with a span and add a title attribute providing a tooltip which explains this column, eg: indicates which templates generated the x509 certificate.
  • the table doesn't use the available width

Let's look at the certs admin:

Image

Can we make it look identical? Except the "organization" field which is implicit (we can omit it and replace it with the "Template" column).
Let's also link the CA (with target="_blank").

instance.config.device.organization = validated_data.get(
"organization"
)
DeviceCertificate.objects.filter(config=instance.config).delete()

@nemesifier nemesifier Jul 13, 2026

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 cleanup only runs in the branch where no config payload was supplied. If a request changes both organization and config, _update_config() runs first while device.organization still points to the old organization, and the organization-change cleanup is skipped. With certificate templates this path now also has to keep DeviceCertificate rows in sync with the final organization/templates state. Please cover this combined organization-plus-config update path so old templates and device certificates are cleaned consistently before applying the new configuration.

if self._has_config():
self.config.set_status_modified()

self._initial_mac_address = self.mac_address

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 advances _initial_mac_address after every save, regardless of update_fields. If mac_address is dirty in memory but saved with save(update_fields=["name"]), the new MAC is not persisted but _initial_mac_address is still updated to the dirty value. A later save that actually includes mac_address will compare against the already-advanced initial value and skip certificate regeneration, leaving the certificate CN and MAC OID stale. Please make the initial-value update respect update_fields, or pass the saved fields into _check_changed_fields() so fields that were not persisted do not advance their _initial_* values.

vpn_list = config.templates.filter(type="vpn").values_list("vpn")
if vpn_list:
config.vpnclient_set.exclude(vpn__in=vpn_list).delete()
DeviceCertificate.objects.filter(config=config).exclude(

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.

Deleting DeviceCertificate rows here happens before config.templates.set(..., clear=True) has enforced required templates. If a required certificate template is omitted from the submitted template list, it will be re-added later, but its old DeviceCertificate has already been deleted and the auto-generated cert revoked. The template remains assigned, but certificate material is unnecessarily rotated. Please wait until the final template set is known, including required templates, or rely on the M2M lifecycle handler to remove only rows that are truly no longer assigned.

context.update(self.device._get_group().get_context())
# Add predefined variables
context.update(self.get_vpn_context())
context.update(self.get_cert_context())

@nemesifier nemesifier Jul 13, 2026

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 adds a devicecertificate_set query to every config context render, including configs that do not use certificate templates. In bulk status updates, _update_related_config_status() prefetches VPN clients and templates but not device certificates, so each config can now pay an extra query. Please consider reusing prefetches where available and prefetching device certificates in bulk paths, or avoiding this query when the config has no certificate templates.

)
return value

def validate(self, data):

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.

TemplateSerializer already inherits from ValidatedModelSerializer, which calls model full_clean(). This method repeats much of the same certificate-template validation that already lives in AbstractTemplate.clean() and helper methods, causing duplicate active-config and blueprint assignment checks and increasing the chance API behavior diverges from model/admin behavior. Please keep the core validation in the model and let the serializer rely on model validation, using only a small shared normalization helper if the API needs normalized values copied into validated_data.

cn = self._get_common_name()
self._auto_create_cert(name=self.config.device.name, common_name=cn)

def _get_common_name(self):

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 common-name generation duplicates the existing VPN client logic in AbstractVpnClient._get_common_name(): same COMMON_NAME_FORMAT, same truncation rules, same MAC fallback, and same shortuuid suffix. Please extract this to a shared helper that accepts a device and returns the generated common name, then use it from both VPN clients and standalone device certificates.

},
]

def _auto_create_cert(self, name, common_name):

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.

A lot of the certificate lifecycle code in this model repeats existing VPN certificate provisioning behavior: auto X.509 creation, organization assignment, default client extensions, CA-to-cert attribute copying, and revoke-on-delete. These are security-sensitive rules, so maintaining multiple copies is error-prone. Please factor out the stable shared pieces, at least common-name generation, certificate attribute copying, default client extensions, organization assignment, and revocation behavior.

if self._state.adding:
return
try:
current = self.__class__.objects.get(pk=self.pk)

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 extra query runs on every existing template validation just to know whether protected fields changed. The project already uses an _initial_* pattern for this in AbstractDevice. Please apply the same approach to the protected template fields, likely type, ca_id, and blueprint_cert_id, so validation can compare against initial values without fetching the current row each time.

@mock.patch(
"openwisp_controller.config.tasks.regenerate_device_certificates_task.delay"
)
def test_hardware_drift_signal_triggers_on_name_change(self, mocked_task):

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.

Please avoid the current unclear wording around this behavior in tests, user-facing text, and docs. The feature is certificate regeneration after device identity fields change, currently name and MAC address, and that wording is much clearer. More generally, these new tests would be easier to maintain if split by feature area, for example signal-trigger tests, regeneration task tests, and DeviceCertificate model tests instead of adding all of them to the broad TestDevice class.

@openwisp-companion

Copy link
Copy Markdown

CI Failure Analysis

Hello @stktyagi,
(Analysis for commit 8c6e35a)

  1. Code Style/QA: A Flake8 error (F401 - imported but unused) was detected.
  • Fix: Remove the unused import openwisp_controller.config.controller.views.DeviceChecksumView from ./openwisp_controller/config/handlers.py.
  1. Test Failure: Several tests failed with AssertionError or similar exceptions, indicating issues in the test logic or application code. The specific tests that failed are marked with [31;1m (red) in the output.
  • Fix: Review the failing tests, compare the expected vs. actual results, and correct the underlying code or test logic.

stktyagi added 2 commits July 16, 2026 14:55
Fixed failing tests after resolving merge conflict
Fixed formatting via formatter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement gsoc Part of a Google Summer of Code project

Projects

Development

Successfully merging this pull request may close these issues.

4 participants