Skip to content

[change] Made Site name configuration and uppdated OpenVPN template#566

Open
pandafy wants to merge 3 commits intomasterfrom
default-site-and-openvpn
Open

[change] Made Site name configuration and uppdated OpenVPN template#566
pandafy wants to merge 3 commits intomasterfrom
default-site-and-openvpn

Conversation

@pandafy
Copy link
Member

@pandafy pandafy commented Feb 20, 2026

Made Site name configuration and uppdated OpenVPN template

@pandafy pandafy moved this from To do (general) to Needs review in OpenWISP Contributor's Board Feb 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Added a conditional default OpenVPN client log path in the load-init script: when a Template with an openvpn config is created, its config is validated and, if present, config["openvpn"][0]["log"] is set to "/var/log/tun0.log" before save. Introduced a new public function update_default_site() that reads DASHBOARD_DOMAIN from the environment and updates the Django Sites default entry when the current site name or domain equals "example.com". update_default_site() is invoked after admin creation in create_default_topology(vpn) and in the script main flow after admin creation. The OpenVPN JSON replaces "cipher" with explicit "cipher": "AES-128-GCM" and adds "data_ciphers": "AES-128-GCM:none".

Sequence Diagram(s)

sequenceDiagram
    participant Script
    participant Template
    participant AdminCreator
    participant Env
    participant Sites

    Script->>Template: instantiate Template(...) and full_clean()
    Template->>Template: if config.openvpn exists
    Template->>Template: set config.openvpn[0].log = "/var/log/tun0.log"
    Template->>Template: save()
    Script->>AdminCreator: create admin user
    AdminCreator-->>Script: admin created
    Script->>Env: read DASHBOARD_DOMAIN
    Script->>Sites: query Site where domain or name == "example.com"
    alt match found
        Script->>Sites: update Site.domain and Site.name = DASHBOARD_DOMAIN
        Sites-->>Script: save confirmation
    else
        Sites-->>Script: no change
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely incomplete. It only repeats the title without providing substantive details about the changes, missing required sections like objective explanation, testing details, and issue reference from the template. Expand the description to include what was changed and why, reference an existing issue with 'Closes #', document manual testing performed, and indicate if new tests were added or existing tests updated.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title is partially related to the changeset. It mentions 'Site name configuration' and 'OpenVPN template', which are real aspects of the changes, but contains a typo ('uppdated' instead of 'updated') and doesn't clearly indicate the specific improvements made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch default-site-and-openvpn

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
images/openwisp_dashboard/load_init_data.py (2)

205-205: Use settings.SITE_ID to reliably identify the default site instead of .first().

Site.objects.first() returns the lowest-PK site, which may not be the site identified by settings.SITE_ID in multi-site deployments. Use Site.objects.get_current() (which respects settings.SITE_ID and the request-cache) or the explicit lookup below:

♻️ Proposed fix
-        site = Site.objects.first()
+        site = Site.objects.get(id=settings.SITE_ID)

You would also need to handle the Site.DoesNotExist exception if SITE_ID points to a missing record:

+        try:
+            site = Site.objects.get(id=settings.SITE_ID)
+        except Site.DoesNotExist:
+            return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/load_init_data.py` at line 205, Replace the fragile
Site.objects.first() lookup with a SITE_ID-aware lookup: import
django.conf.settings and use Site.objects.get_current() (which respects
settings.SITE_ID) or Site.objects.get(pk=settings.SITE_ID) inside the same
function where site is needed (e.g., around the Site variable assignment in
load_init_data.py). Add a try/except for Site.DoesNotExist to handle a missing
SITE_ID record and log or raise a clear error, ensuring subsequent code uses the
resolved site object.

208-210: Call site.full_clean() before site.save() to validate the env-sourced domain.

dashboard_domain comes from os.environ without any format validation. Calling site.full_clean() would apply Django's built-in URLValidator/field-level validators on the domain before it is persisted, preventing malformed values from being silently written to the database.

♻️ Proposed fix
             site.name = dashboard_domain
             site.domain = dashboard_domain
+            site.full_clean()
             site.save()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/load_init_data.py` around lines 208 - 210, The code
assigns dashboard_domain (from os.environ) directly to site.name and site.domain
then calls site.save(); instead call site.full_clean() before saving to run
Django field validators (including URL/Domain validators) and raise/handle
ValidationError for malformed env values—so after setting site.name and
site.domain invoke site.full_clean() and only then site.save(), ensuring you
catch or log ValidationError for dashboard_domain as needed.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f378f3 and 400f4d0.

📒 Files selected for processing (2)
  • images/openwisp_dashboard/load_init_data.py
  • images/openwisp_dashboard/openvpn.json
🧰 Additional context used
🪛 GitHub Check: CodeQL
images/openwisp_dashboard/load_init_data.py

[failure] 207-207: Incomplete URL substring sanitization
The string example.com may be at an arbitrary position in the sanitized URL.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI Build
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@images/openwisp_dashboard/load_init_data.py`:
- Line 207: The conditional using `"example.com" in [site.name, site.domain]`
should be replaced with explicit equality checks to avoid the CodeQL false
positive; update the if in load_init_data.py so it directly compares site.name
== "example.com" or site.domain == "example.com" (while preserving the existing
`site` and `dashboard_domain` checks) so intent is unambiguous and behavior
unchanged.

In `@images/openwisp_dashboard/openvpn.json`:
- Around line 14-15: Change the incorrect OpenVPN config field and format:
replace the unrecognized "data_cipher" key with "data_ciphers" and convert its
string value into an array of cipher objects (each object containing "cipher"
and "optional" keys) instead of the colon-separated string; also remove the
"none" (unencrypted) option so only valid encrypted ciphers like "AES-128-GCM"
are listed. Ensure the final "data_ciphers" is an array of objects (e.g., {
"cipher": "...", "optional": false }) to match the netjsonconfig v1.1.0+ OpenVPN
schema.

---

Nitpick comments:
In `@images/openwisp_dashboard/load_init_data.py`:
- Line 205: Replace the fragile Site.objects.first() lookup with a SITE_ID-aware
lookup: import django.conf.settings and use Site.objects.get_current() (which
respects settings.SITE_ID) or Site.objects.get(pk=settings.SITE_ID) inside the
same function where site is needed (e.g., around the Site variable assignment in
load_init_data.py). Add a try/except for Site.DoesNotExist to handle a missing
SITE_ID record and log or raise a clear error, ensuring subsequent code uses the
resolved site object.
- Around line 208-210: The code assigns dashboard_domain (from os.environ)
directly to site.name and site.domain then calls site.save(); instead call
site.full_clean() before saving to run Django field validators (including
URL/Domain validators) and raise/handle ValidationError for malformed env
values—so after setting site.name and site.domain invoke site.full_clean() and
only then site.save(), ensuring you catch or log ValidationError for
dashboard_domain as needed.

Comment on lines 14 to 15
"data_cipher": "AES-128-GCM:none",
"cipher": "AES-128-GCM",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "openvpn.json" -type f

Repository: openwisp/docker-openwisp

Length of output: 108


🏁 Script executed:

# Search for netjsonconfig references and schema definitions
fd -e json -e py -e md "netjson" --exec grep -l "data.cipher" {} \;
rg -n "data.cipher|data-cipher" --type py --type json -C 2 | head -100

Repository: openwisp/docker-openwisp

Length of output: 415


🏁 Script executed:

# Look for netjsonconfig configuration in the repo
fd -type f -name "*.py" | xargs grep -l "netjsonconfig\|openvpn" | head -10

Repository: openwisp/docker-openwisp

Length of output: 238


🏁 Script executed:

# Search for netjsonconfig in the repository
rg "netjsonconfig|data.cipher|data-cipher" -C 3 | head -150

Repository: openwisp/docker-openwisp

Length of output: 2262


🏁 Script executed:

# Look for python files that might contain configuration or schema
find . -name "*.py" -type f | xargs grep -l "data_cipher\|data-cipher" 2>/dev/null

Repository: openwisp/docker-openwisp

Length of output: 52


🏁 Script executed:

# Check if there are any netjsonconfig configuration files or documentation
find . -name "*.md" -o -name "*.txt" -o -name "requirements*.txt" | xargs grep -l "netjsonconfig" 2>/dev/null | head -5

Repository: openwisp/docker-openwisp

Length of output: 50


🏁 Script executed:

# Look at the openvpn.json file in context to understand the structure
cat -n ./images/openwisp_dashboard/openvpn.json | head -30

Repository: openwisp/docker-openwisp

Length of output: 970


🏁 Script executed:

# Search for any documentation or references to netjsonconfig schema
rg "openvpn" -t md -C 3 | head -100

Repository: openwisp/docker-openwisp

Length of output: 216


🏁 Script executed:

# Check if there's a setup.py or requirements file that shows netjsonconfig version
cat requirements*.txt setup.py 2>/dev/null | grep -i netjsonconfig

Repository: openwisp/docker-openwisp

Length of output: 50


🏁 Script executed:

# Look for any comments or documentation about the openvpn.json file
find . -name "*.md" -o -name "*.rst" | xargs grep -l "openvpn\|data_cipher" 2>/dev/null

Repository: openwisp/docker-openwisp

Length of output: 204


🏁 Script executed:

# Check the load_init_data.py to understand how openvpn.json is used
cat -n ./images/openwisp_dashboard/load_init_data.py | grep -A 20 -B 5 "openvpn"

Repository: openwisp/docker-openwisp

Length of output: 3841


🌐 Web query:

netjsonconfig OpenVPN data_cipher data-ciphers field name schema

💡 Result:

In netjsonconfig’s OpenVPN backend schema, the OpenVPN --data-ciphers option is represented by the snake_case field:

  • data_ciphers: a list of objects, where each object must contain:

Related field:

So the schema field name is data_ciphers (not data_cipher, and not data-ciphers). netjsonconfig added support for this option in v1.1.0. (netjsonconfig.openwisp.org)

Citations:


🌐 Web query:

netjsonconfig openvpn json schema documentation

💡 Result:

  • Primary documentation (OpenVPN backend + schema overview): netjsonconfig “OpenVPN Backend” docs (includes the “OpenVPN backend schema” section and links to the schema source). [1]
  • OpenVPN JSON-Schema source (authoritative schema definition used by the backend): netjsonconfig/backends/openvpn/schema.py in the openwisp/netjsonconfig repo (raw source). [2]
  • How schemas/validation work in netjsonconfig (general docs): “Basic concepts” (schema inheritance + validate behavior). [3]
  • Project entry point / repo (for browsing other backend schemas and docs versions): openwisp/netjsonconfig on GitHub. [4]
  • Underlying NetJSON spec note (why unknown properties may be ignored): NetJSON RFC/draft text describing how unrecognized custom members should be handled. [5]

Sources: [1] (netjsonconfig.openwisp.org) [2] (raw.githubusercontent.com) [3] (netjsonconfig.openwisp.org) [4] (github.com) [5] (netjson.org)

Citations:


🏁 Script executed:

# Check if there's a CHANGES.rst or documentation about field names
cat ./CHANGES.rst | grep -A 5 -B 5 "data.cipher\|netjsonconfig"

Repository: openwisp/docker-openwisp

Length of output: 698


🏁 Script executed:

# Look for any validation or schema comments in Python files
rg "data_cipher|data-cipher" -B 3 -A 3

Repository: openwisp/docker-openwisp

Length of output: 519


🌐 Web query:

netjsonconfig data_ciphers format list objects cipher optional example

💡 Result:

In netjsonconfig’s OpenVPN backend, the data_ciphers field is:

  • a list (array) of objects (dicts)
  • each object must contain:
    • cipher (string)
    • optional (boolean)
      as described in the OpenVPN backend schema docs. [1]
      (data_ciphers_fallback is a single string cipher name.) [1]

Example (NetJSONConfig / OpenVPN instance)

{
  "openvpn": [
    {
      "name": "example-vpn",
      "mode": "server",
      "proto": "udp",
      "dev": "tun0",
      "dev_type": "tun",

      "data_ciphers": [
        { "cipher": "AES-256-GCM", "optional": false },
        { "cipher": "AES-128-GCM", "optional": true }
      ],
      "data_ciphers_fallback": "AES-256-CBC"
    }
  ]
}

(Choose cipher names from the OpenVPN backend’s allowed cipher list referenced by the docs.) [1]

Note: data-ciphers support was added to netjsonconfig in v1.1.0 (2024-11-22). [2]

Sources:
[1] netjsonconfig OpenVPN backend docs (schema table including data_ciphers) https://netjsonconfig.openwisp.org/en/stable/backends/openvpn.html
[2] netjsonconfig changelog (feature: “Added data-ciphers option for OpenVPN”) https://netjsonconfig.openwisp.org/en/stable/general/changelog.html


🏁 Script executed:

# Check if there are any test files or examples in the repo showing correct format
find . -name "*.json" -o -name "*.py" | xargs grep -l "data_cipher" 2>/dev/null

Repository: openwisp/docker-openwisp

Length of output: 108


Fix field name, format, and remove unencrypted cipher option.

The configuration has three critical issues:

  1. Wrong field name: data_cipher should be data_ciphers (plural). With the incorrect name, netjsonconfig silently ignores the entire field, meaning the data cipher configuration is never applied.

  2. Wrong format: The field expects an array of objects (each with cipher and optional keys), not a colon-separated string. Current format "AES-128-GCM:none" is invalid for netjsonconfig's OpenVPN schema.

  3. Security issue: The value "none" disables data channel encryption and is only for debugging. Shipping it in production allows clients to transmit VPN traffic in cleartext.

Correct the configuration:

Fix
-      "data_cipher": "AES-128-GCM:none",
+      "data_ciphers": [
+        {"cipher": "AES-128-GCM", "optional": false}
+      ],

(Per netjsonconfig v1.1.0+ OpenVPN backend schema, data_ciphers is a list of cipher objects; data_cipher is not a recognized field and will be silently ignored.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"data_cipher": "AES-128-GCM:none",
"cipher": "AES-128-GCM",
"data_ciphers": [
{"cipher": "AES-128-GCM", "optional": false}
],
"cipher": "AES-128-GCM",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/openvpn.json` around lines 14 - 15, Change the
incorrect OpenVPN config field and format: replace the unrecognized
"data_cipher" key with "data_ciphers" and convert its string value into an array
of cipher objects (each object containing "cipher" and "optional" keys) instead
of the colon-separated string; also remove the "none" (unencrypted) option so
only valid encrypted ciphers like "AES-128-GCM" are listed. Ensure the final
"data_ciphers" is an array of objects (e.g., { "cipher": "...", "optional":
false }) to match the netjsonconfig v1.1.0+ OpenVPN schema.

@github-project-automation github-project-automation bot moved this from Needs review to In progress in OpenWISP Contributor's Board Feb 20, 2026
@pandafy pandafy force-pushed the default-site-and-openvpn branch from 400f4d0 to a586871 Compare February 20, 2026 09:19
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
images/openwisp_dashboard/load_init_data.py (1)

205-205: Prefer Site.objects.get(pk=settings.SITE_ID) over Site.objects.first().

first() may return the wrong site when multiple sites exist. The SITE_ID setting unambiguously identifies the "current" site intended by Django's sites framework.

♻️ Proposed refactor
-        site = Site.objects.first()
+        site = Site.objects.get(pk=settings.SITE_ID)

Then simplify the guard (the object is guaranteed to exist):

-        if site and "example.com" in [site.name, site.domain] and dashboard_domain:
+        if (site.name == "example.com" or site.domain == "example.com") and dashboard_domain:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/load_init_data.py` at line 205, Replace the fragile
query Site.objects.first() with an explicit lookup using
Site.objects.get(pk=settings.SITE_ID): import settings if not present, call
Site.objects.get(pk=settings.SITE_ID) where the variable site is assigned, and
then remove or simplify any subsequent existence guard (since SITE_ID should
always resolve to an existing Site); reference the Site model and
settings.SITE_ID in the change and ensure any DoesNotExist handling is adjusted
or removed accordingly.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 400f4d0 and a586871.

📒 Files selected for processing (2)
  • images/openwisp_dashboard/load_init_data.py
  • images/openwisp_dashboard/openvpn.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • images/openwisp_dashboard/openvpn.json
🧰 Additional context used
🪛 GitHub Check: CodeQL
images/openwisp_dashboard/load_init_data.py

[failure] 207-207: Incomplete URL substring sanitization
The string example.com may be at an arbitrary position in the sanitized URL.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI Build
🔇 Additional comments (1)
images/openwisp_dashboard/load_init_data.py (1)

255-256: LGTM — placement of update_default_site() is correct.

Calling it right after create_admin() and before VPN setup ensures the site domain is updated before any subsequent steps that might depend on it. The inline conditional import inside the function keeps it self-contained.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@images/openwisp_dashboard/load_init_data.py`:
- Around line 136-138: The code unguardedly accesses
template.config['openvpn'][0], which can raise KeyError/IndexError; update the
block around template.full_clean(), template.config and template.save() to
defensively ensure template.config has an 'openvpn' key that is a non-empty list
(e.g., if 'openvpn' missing or not a list, set it to a list with at least one
dict), then set the first element's 'log' value to '/var/log/tun0.log' and call
template.save(); reference the existing symbols template.config,
template.full_clean, and template.save when making this change.

---

Duplicate comments:
In `@images/openwisp_dashboard/load_init_data.py`:
- Line 207: Replace the substring membership check "example.com" in [site.name,
site.domain] with explicit equality comparisons to avoid the CodeQL false
positive: change the conditional that checks site and dashboard_domain (the one
using site.name and site.domain) to test site.name == "example.com" or
site.domain == "example.com" so the intent is unambiguous and substring matching
is eliminated.

---

Nitpick comments:
In `@images/openwisp_dashboard/load_init_data.py`:
- Line 205: Replace the fragile query Site.objects.first() with an explicit
lookup using Site.objects.get(pk=settings.SITE_ID): import settings if not
present, call Site.objects.get(pk=settings.SITE_ID) where the variable site is
assigned, and then remove or simplify any subsequent existence guard (since
SITE_ID should always resolve to an existing Site); reference the Site model and
settings.SITE_ID in the change and ensure any DoesNotExist handling is adjusted
or removed accordingly.

- Updated ciphers
- Added login to OpenVPN template
@pandafy pandafy force-pushed the default-site-and-openvpn branch from a586871 to 1b3ff7e Compare February 20, 2026 09:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
images/openwisp_dashboard/openvpn.json (1)

14-14: ⚠️ Potential issue | 🔴 Critical

data_ciphers still has the wrong value type, and none remains a security risk.

The field name was corrected to data_ciphers (plural), but the value "AES-128-GCM:none" is still a colon-separated string. Per the netjsonconfig OpenVPN schema, data_ciphers expects a list of dicts, each with a cipher and optional key. A plain string is not a valid value and will be treated as an unrecognized type — according to the NetJSON spec, any unrecognized property will be ignored — so this field has no effect at runtime.

Additionally, none in the cipher list disables data-channel encryption and should never ship in production config.

🔧 Proposed fix
-      "data_ciphers": "AES-128-GCM:none",
+      "data_ciphers": [
+        {"cipher": "AES-128-GCM", "optional": false}
+      ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/openvpn.json` at line 14, Replace the string value
for the data_ciphers field with a list of dicts conforming to the NetJSON
OpenVPN schema (each dict must have cipher and optional keys); e.g. remove the
insecure "none" entry and use an array like
[{"cipher":"AES-128-GCM","optional":false}] so data_ciphers is an actual list of
objects rather than a colon-separated string and thus will be honored at
runtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@images/openwisp_dashboard/load_init_data.py`:
- Around line 206-216: Site.objects.get(pk=settings.SITE_ID) can raise
Site.DoesNotExist and never returns None, so remove the dead "if site" guard and
wrap the lookup in a try/except Site.DoesNotExist block; on exception, skip the
update (or log a clear message) instead of letting the init script crash, and
keep the existing update logic (assigning dashboard_domain to site.name and
site.domain, calling site.full_clean() and site.save()) inside the try block so
it only runs when the Site exists and dashboard_domain is set.
- Line 137: The conditional at the top of load_init_data.py redundantly checks
length; replace the expression if template.config.get("openvpn", []) and
len(template.config["openvpn"]) > 0 with a single truthy check using
template.config.get("openvpn") so the code reads simply to test for a non-empty
openvpn list; update any nearby comments to reflect the simplified check.
- Around line 136-139: The code calls template.full_clean(), then mutates
template.config by setting template.config["openvpn"][0]["log"] and immediately
calls template.save() which bypasses validation of the mutated state; update the
flow so the mutation is validated by either moving the openvpn log assignment
before the initial template.full_clean() or, if keeping the current order, call
template.full_clean() again after setting template.config["openvpn"][0]["log"]
and before template.save(); reference the existing symbols
template.full_clean(), template.config["openvpn"][0][\"log\"], and
template.save() when making the change.

---

Duplicate comments:
In `@images/openwisp_dashboard/openvpn.json`:
- Line 14: Replace the string value for the data_ciphers field with a list of
dicts conforming to the NetJSON OpenVPN schema (each dict must have cipher and
optional keys); e.g. remove the insecure "none" entry and use an array like
[{"cipher":"AES-128-GCM","optional":false}] so data_ciphers is an actual list of
objects rather than a colon-separated string and thus will be honored at
runtime.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a586871 and 4c7f194.

📒 Files selected for processing (2)
  • images/openwisp_dashboard/load_init_data.py
  • images/openwisp_dashboard/openvpn.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI Build
🔇 Additional comments (2)
images/openwisp_dashboard/load_init_data.py (2)

261-262: LGTM — update_default_site() is correctly placed after create_admin().


219-242: AI summary inconsistency: update_default_site() is not called inside create_default_topology.

The AI summary states that update_default_site() is "invoked after create_admin() in two locations: within create_default_topology(vpn) flow." The function at lines 219–242 contains no such call. The only invocation is at line 262 in the __main__ block.

Comment on lines 136 to 139
template.full_clean()
if template.config.get("openvpn", []) and len(template.config["openvpn"]) > 0:
template.config["openvpn"][0]["log"] = "/var/log/tun0.log"
template.save()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Config mutated after full_clean() but saved without re-validation.

template.full_clean() is called at line 136, then config is modified at line 138, and template.save() is called at line 139 without a second full_clean(). The log path is simple and unlikely to break, but mutating validated state and then skipping re-validation is incorrect ordering; if the schema tightens, this silently bypasses it.

🔧 Proposed fix
     template.full_clean()
     if template.config.get("openvpn", []) and len(template.config["openvpn"]) > 0:
         template.config["openvpn"][0]["log"] = "/var/log/tun0.log"
+        template.full_clean()
     template.save()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/load_init_data.py` around lines 136 - 139, The code
calls template.full_clean(), then mutates template.config by setting
template.config["openvpn"][0]["log"] and immediately calls template.save() which
bypasses validation of the mutated state; update the flow so the mutation is
validated by either moving the openvpn log assignment before the initial
template.full_clean() or, if keeping the current order, call
template.full_clean() again after setting template.config["openvpn"][0]["log"]
and before template.save(); reference the existing symbols
template.full_clean(), template.config["openvpn"][0][\"log\"], and
template.save() when making the change.

default=True,
)
template.full_clean()
if template.config.get("openvpn", []) and len(template.config["openvpn"]) > 0:
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant length guard.

template.config.get("openvpn", []) already evaluates falsy for an empty list, making and len(template.config["openvpn"]) > 0 a no-op.

♻️ Proposed simplification
-    if template.config.get("openvpn", []) and len(template.config["openvpn"]) > 0:
+    if template.config.get("openvpn"):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/load_init_data.py` at line 137, The conditional at
the top of load_init_data.py redundantly checks length; replace the expression
if template.config.get("openvpn", []) and len(template.config["openvpn"]) > 0
with a single truthy check using template.config.get("openvpn") so the code
reads simply to test for a non-empty openvpn list; update any nearby comments to
reflect the simplified check.

Comment on lines +206 to +216
site = Site.objects.get(pk=settings.SITE_ID)
dashboard_domain = os.environ.get("DASHBOARD_DOMAIN", "")
if (
site
and (site.name == "example.com" or site.domain == "example.com")
and dashboard_domain
):
site.name = dashboard_domain
site.domain = dashboard_domain
site.full_clean()
site.save()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unhandled Site.DoesNotExist and dead if site guard.

Site.objects.get(pk=settings.SITE_ID) either returns a Site instance or raises Site.DoesNotExist — it never returns None. Therefore:

  1. The if site check on line 209 is dead code and gives a false sense of safety.
  2. If settings.SITE_ID is not present in the database (e.g., first deploy before migrations), the unhandled DoesNotExist exception will crash the entire init script.
🔧 Proposed fix
-        site = Site.objects.get(pk=settings.SITE_ID)
-        dashboard_domain = os.environ.get("DASHBOARD_DOMAIN", "")
-        if (
-            site
-            and (site.name == "example.com" or site.domain == "example.com")
-            and dashboard_domain
-        ):
-            site.name = dashboard_domain
-            site.domain = dashboard_domain
-            site.full_clean()
-            site.save()
+        dashboard_domain = os.environ.get("DASHBOARD_DOMAIN", "")
+        if not dashboard_domain:
+            return
+        try:
+            site = Site.objects.get(pk=settings.SITE_ID)
+        except Site.DoesNotExist:
+            return
+        if site.name == "example.com" or site.domain == "example.com":
+            site.name = dashboard_domain
+            site.domain = dashboard_domain
+            site.full_clean()
+            site.save()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@images/openwisp_dashboard/load_init_data.py` around lines 206 - 216,
Site.objects.get(pk=settings.SITE_ID) can raise Site.DoesNotExist and never
returns None, so remove the dead "if site" guard and wrap the lookup in a
try/except Site.DoesNotExist block; on exception, skip the update (or log a
clear message) instead of letting the init script crash, and keep the existing
update logic (assigning dashboard_domain to site.name and site.domain, calling
site.full_clean() and site.save()) inside the try block so it only runs when the
Site exists and dashboard_domain is set.

"comp_lzo": "no",
"auth": "SHA1",
"cipher": "none",
"data_ciphers": "AES-128-GCM:none",
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to add also data_ciphers_fallback which is the new cipher fallback on newer versions.

@nemesifier nemesifier added the enhancement New feature or request label Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants