Skip to content

[subnet_division] Fix overlapping subnets with different prefix sizes #864#1252

Open
Piyushkhobragade wants to merge 1 commit intoopenwisp:masterfrom
Piyushkhobragade:issues/864-subnet-division-overlap
Open

[subnet_division] Fix overlapping subnets with different prefix sizes #864#1252
Piyushkhobragade wants to merge 1 commit intoopenwisp:masterfrom
Piyushkhobragade:issues/864-subnet-division-overlap

Conversation

@Piyushkhobragade
Copy link
Copy Markdown

@Piyushkhobragade Piyushkhobragade commented Mar 3, 2026

Fixes #864.

The test_multiple_subnetdivision_rules_different_sizes test was failing because subnets with varying prefix lengths (like /31 and /29) were physically overlapping.

What was happening:
The old logic relied on database ordering (.last()) to figure out the next available subnet. This doesn't work well in SQLite when evaluating different prefix lengths, causing the IP collisions.

The fix:

Dropped the database-ordering approach in get_max_subnet and create_subnets.

Switched to calculating the next valid block purely mathematically using netaddr.IPSet.

Added a guard to ensure we only create the exact number of subnets required by the rule. Previously, keeping the math contiguous was forcing the creation of placeholder subnets, which caused the AssertionError: 4 != 2 in the test.

Tests are passing locally.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 3, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 47d21e6a-15b7-4ddc-be4f-5a749ba7a655

📥 Commits

Reviewing files that changed from the base of the PR and between 00d4275 and 511a951.

📒 Files selected for processing (1)
  • openwisp_controller/subnet_division/rule_types/base.py
 ________________________________________
< Even Chuck Norris needs a code review. >
 ----------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

Reworks subnet-division provisioning to be rule-aware and incremental. get_max_subnet now prefers computing the maximum from subnets already provisioned for the given division_rule (using IPNetwork normalization) and, if none exist, scans the master subnet for the next available subnet (using IPNetwork/IPSet), raising ValidationError when space is exhausted. create_subnets creates only the remaining subnets one-by-one, saving each and creating SubnetDivisionIndex entries. create_ips saves each IpAddress individually before creating corresponding SubnetDivisionIndex entries. Minor imports added (gettext_lazy as _, transaction) and attrgetter is imported inline in get_config.

Sequence Diagram(s)

sequenceDiagram
    participant Rule as DivisionRuleHandler
    participant IPTools as IPNetwork/IPSet
    participant DB as Database(Subnet, IpAddress, SubnetDivisionIndex)

    Note over Rule,DB: Provisioning start
    Rule->>DB: query Subnet WHERE division_rule = X
    alt existing subnets for rule
        DB-->>Rule: return existing subnets
        Rule->>IPTools: normalize and compute max/next subnet from returned subnets
    else no subnets for rule
        Rule->>DB: fetch master subnet for config
        DB-->>Rule: return master subnet
        Rule->>IPTools: iterate master to find next unused subnets using IPSet
    end

    loop for each subnet to create
        Rule->>DB: create/save Subnet (single)
        DB-->>Rule: return Subnet id
        Rule->>DB: create SubnetDivisionIndex for subnet
        loop per IP to provision in subnet
            Rule->>DB: create/save IpAddress (single)
            DB-->>Rule: return IpAddress id
            Rule->>DB: create SubnetDivisionIndex for IP
        end
    end

    Note over Rule,DB: destroy_provisioned_subnets_ips deletes Subnet rows -> DB cascades IpAddress and Index deletions
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the specific issue being fixed (#864) and describes the main problem (overlapping subnets with different prefix sizes) that the changes address.
Description check ✅ Passed The description covers the issue being fixed, explains the root cause, and describes the solution. All required template sections are present and substantively completed.
Linked Issues check ✅ Passed The PR addresses the core objective from #864: ensuring multiple SubnetDivisionRule entries with different sizes can provision subnets without collisions by replacing database-ordering logic with mathematical IPSet-based calculations.
Out of Scope Changes check ✅ Passed All changes are focused on fixing the overlapping subnet issue in the BaseSubnetDivisionRuleType class through improved mathematical calculations and rule-aware subnet creation logic.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Tip

CodeRabbit can use TruffleHog to scan for secrets in your code with verification capabilities.

Add a TruffleHog config file (e.g. trufflehog-config.yml, trufflehog.yml) to your project to customize detectors and scanning behavior. The tool runs only when a config file is present.

Copy link
Copy Markdown
Contributor

@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

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

Inline comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 151-199: The class BaseSubnetDivisionRuleType contains duplicate
method definitions for create_ips and destroy_provisioned_subnets_ips; remove
the earlier (dead) duplicates so each method is defined only once and the later
authoritative implementations remain. Locate the duplicate create_ips and
destroy_provisioned_subnets_ips methods in BaseSubnetDivisionRuleType, delete
the redundant (overridden) versions, and ensure the remaining methods correctly
reference symbols used inside them (create_ips uses IpAddress,
SubnetDivisionIndex and division_rule fields; destroy_provisioned_subnets_ips
uses cls.get_config, rule type string, config.subnetdivisionindex_set and
Subnet) so no imports or references are broken. After removal, run tests/lint to
ensure no missing references and update or consolidate any docstrings/comments
if needed.
- Around line 249-255: The existing_count query is overcounting because the
filter on subnetdivisionindex__rule joins multiple SubnetDivisionIndex rows per
subnet; change the counting query in the block using existing_count (the
Subnet.objects.filter(... subnetdivisionindex__rule=division_rule).count() call)
to count distinct subnets (e.g. add .distinct() or use
.values('id').distinct().count()) so existing_count reflects unique Subnet
instances before comparing to division_rule.number_of_subnets and returning
early; this adjustment ensures create_ips (which adds multiple indexes per
subnet) won't inflate the count.
- Around line 220-225: The current get_max_subnet() uses
existing_rule_subnets.order_by("subnet").last(), which relies on DB text
ordering and is backend-dependent; instead, fetch the matching subnets
(existing_rule_subnets) into Python (e.g., values_list('subnet', flat=True)),
parse them with the ipaddress module (ipaddress.ip_network) and compute the
numeric maximum (max(..., key=lambda n: int(n.network_address))). Return that
network as a string so .next() allocates from the correct numeric maximum;
update the code around existing_rule_subnets and the .order_by("subnet").last()
usage accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c266e80 and 6527db1.

📒 Files selected for processing (1)
  • openwisp_controller/subnet_division/rule_types/base.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.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
🧰 Additional context used
🧠 Learnings (3)
📚 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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.py
🧬 Code graph analysis (1)
openwisp_controller/subnet_division/rule_types/base.py (3)
openwisp_controller/subnet_division/models.py (1)
  • SubnetDivisionIndex (12-15)
openwisp_controller/subnet_division/rule_types/vpn.py (1)
  • destroy_provisioned_subnets_ips (57-65)
openwisp_controller/subnet_division/rule_types/device.py (1)
  • destroy_provisioned_subnets_ips (39-43)

@Piyushkhobragade
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 3, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (1)
openwisp_controller/subnet_division/rule_types/base.py (1)

151-199: ⚠️ Potential issue | 🟠 Major

Remove duplicate method definitions to avoid dead code and drift.

create_ips and destroy_provisioned_subnets_ips are defined here, but they are redefined again at Line 301 and Line 332. Python keeps the later definitions, so this block is unreachable and risky to maintain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/subnet_division/rule_types/base.py` around lines 151 -
199, The file defines duplicate methods create_ips and
destroy_provisioned_subnets_ips (the earlier definitions in
openwisp_controller/subnet_division/rule_types/base.py) which are shadowed by
later redefinitions; remove the redundant earlier definitions (or merge any
differing logic) so there is exactly one canonical implementation of create_ips
and one of destroy_provisioned_subnets_ips in the class (refer to the methods
named create_ips and destroy_provisioned_subnets_ips within the same
module/class), update any callers if behavior changed, and run tests to confirm
no regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 273-275: The loop that checks subnet containment currently uses
break and thus can silently return fewer subnets than
division_rule.number_of_subnets; replace the break with raising a clear
exception (e.g., ValueError or a specific SubnetAllocationError) when not
ip_network(str(required_subnet)).subnet_of(master_subnet.subnet) so callers know
allocation failed and include context in the message (requested
division_rule.number_of_subnets, current subnet_id/existing_count, and
master_subnet.subnet) to aid debugging; update any callers/tests to expect this
failure mode if applicable.
- Around line 169-187: The SubnetDivisionIndex objects are being created with
ip_id from unsaved IpAddress instances (IpAddress.id is None) inside create_ips,
breaking the FK; to fix, persist the IpAddress rows first and only then create
SubnetDivisionIndex entries: collect IpAddress instances into generated_ips,
call IpAddress.objects.bulk_create(generated_ips) (or save individually),
refresh their IDs (e.g., reload or use saved instances/transaction.on_commit to
ensure IDs are assigned), then build and persist SubnetDivisionIndex records
using the real ip.id values (refer to the create_ips function, generated_ips
list, IpAddress and SubnetDivisionIndex classes and the existing tasks.py
transaction.on_commit pattern as a reference).

---

Duplicate comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 151-199: The file defines duplicate methods create_ips and
destroy_provisioned_subnets_ips (the earlier definitions in
openwisp_controller/subnet_division/rule_types/base.py) which are shadowed by
later redefinitions; remove the redundant earlier definitions (or merge any
differing logic) so there is exactly one canonical implementation of create_ips
and one of destroy_provisioned_subnets_ips in the class (refer to the methods
named create_ips and destroy_provisioned_subnets_ips within the same
module/class), update any callers if behavior changed, and run tests to confirm
no regressions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c266e80 and 4ea1ca1.

📒 Files selected for processing (1)
  • openwisp_controller/subnet_division/rule_types/base.py
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📚 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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.py
🔇 Additional comments (2)
openwisp_controller/subnet_division/rule_types/base.py (2)

219-233: Backend-independent max-subnet selection looks correct.

Using values_list(...).distinct() and numeric IPNetwork comparison avoids DB collation/order differences.


257-265: Distinct subnet counting fix is correct.

Counting distinct subnet IDs prevents join-multiplication from SubnetDivisionIndex rows and aligns the guard with actual subnet count.

Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (1)
openwisp_controller/subnet_division/rule_types/base.py (1)

151-199: 🛠️ Refactor suggestion | 🟠 Major

Remove shadowed duplicate methods to avoid dead and divergent logic.

create_ips and destroy_provisioned_subnets_ips are each defined twice (Line 151/190 and Line 303/335). The earlier versions are overridden and never executed, which leaves dead logic in the class and increases regression risk.

♻️ Proposed cleanup
-    `@staticmethod`
-    def create_ips(config, division_rule, generated_subnets, generated_indexes):
-        ...
-
-    `@classmethod`
-    def destroy_provisioned_subnets_ips(cls, instance, **kwargs):
-        ...
+    # Keep only the later authoritative implementations at Line 303+ and Line 335+

Also applies to: 303-341

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/subnet_division/rule_types/base.py` around lines 151 -
199, The class defines duplicate methods create_ips and
destroy_provisioned_subnets_ips (the early definitions at the top of the class
are shadowed by later ones), so remove or consolidate the redundant definitions
to avoid dead/divergent logic: keep the correct implementation (choose the later
or merged version) and delete the duplicate earlier methods named create_ips and
destroy_provisioned_subnets_ips, updating any internal references to use the
single canonical method names so only one implementation of each exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 271-277: The loop in create_subnets may pick a required_subnet
that overlaps other already-allocated subnets because it only checks containment
against master_subnet; update the loop that iterates subnet_id to, for each
candidate required_subnet, check against existing allocated subnets (and
generated_subnets) for overlaps (using ip_network(...).overlaps(...) or
equivalent) and skip forward (required_subnet =
IPNetwork(str(required_subnet)).next()) until you find a non-overlapping
candidate or run out of space; after each advance re-check containment against
master_subnet.subnet and raise the same ValidationError if no free space
remains. Ensure you reference required_subnet, generated_subnets,
existing_count, division_rule.number_of_subnets and master_subnet.subnet when
implementing the checks.

---

Duplicate comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 151-199: The class defines duplicate methods create_ips and
destroy_provisioned_subnets_ips (the early definitions at the top of the class
are shadowed by later ones), so remove or consolidate the redundant definitions
to avoid dead/divergent logic: keep the correct implementation (choose the later
or merged version) and delete the duplicate earlier methods named create_ips and
destroy_provisioned_subnets_ips, updating any internal references to use the
single canonical method names so only one implementation of each exists.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ea1ca1 and 04150aa.

📒 Files selected for processing (1)
  • openwisp_controller/subnet_division/rule_types/base.py
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📚 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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.py
🔇 Additional comments (1)
openwisp_controller/subnet_division/rule_types/base.py (1)

258-266: Distinct subnet counting fix is correct.

Using .values("id").distinct().count() avoids overcounting caused by the join with subnetdivisionindex__rule and correctly reflects unique provisioned subnets for the rule.

Copy link
Copy Markdown
Contributor

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
openwisp_controller/subnet_division/rule_types/base.py (1)

116-132: 🛠️ Refactor suggestion | 🟠 Major

Consider wrapping provisioning logic in an atomic transaction.

If create_subnets partially succeeds (e.g., creates 2 of 3 subnets) before raising ValidationError, the already-saved subnets persist while the generated_indexes bulk_create at line 131 never runs. This leaves orphaned subnets without corresponding SubnetDivisionIndex entries, and a retry could hit the UniqueConstraint on SubnetDivisionIndex.

Wrapping the provisioning in transaction.atomic() would ensure all-or-nothing behavior:

Proposed fix
     `@classmethod`
     def create_subnets_ips(cls, instance, division_rule, **kwargs):
         try:
             config = cls.get_config(instance)
         except (AttributeError, ObjectDoesNotExist):
             return
 
-        master_subnet = division_rule.master_subnet
-        max_subnet = cls.get_max_subnet(master_subnet, division_rule)
-        generated_indexes = []
-        generated_subnets = cls.create_subnets(
-            config, division_rule, max_subnet, generated_indexes
-        )
-        generated_ips = cls.create_ips(
-            config, division_rule, generated_subnets, generated_indexes
-        )
-        SubnetDivisionIndex.objects.bulk_create(generated_indexes)
-        return {"subnets": generated_subnets, "ip_addresses": generated_ips}
+        with transaction.atomic():
+            master_subnet = division_rule.master_subnet
+            max_subnet = cls.get_max_subnet(master_subnet, division_rule)
+            generated_indexes = []
+            generated_subnets = cls.create_subnets(
+                config, division_rule, max_subnet, generated_indexes
+            )
+            generated_ips = cls.create_ips(
+                config, division_rule, generated_subnets, generated_indexes
+            )
+            SubnetDivisionIndex.objects.bulk_create(generated_indexes)
+        return {"subnets": generated_subnets, "ip_addresses": generated_ips}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/subnet_division/rule_types/base.py` around lines 116 -
132, The create_subnets_ips flow can leave partial DB state if create_subnets or
create_ips raise after some writes; wrap the entire provisioning sequence in a
single atomic transaction so either all created subnets, generated_indexes
(SubnetDivisionIndex.bulk_create) and ips persist or none do. Specifically,
surround the logic inside create_subnets_ips (use transaction.atomic()) around
calls to cls.create_subnets, cls.create_ips and
SubnetDivisionIndex.objects.bulk_create to ensure all-or-nothing commits and
rollbacks on exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Line 5: The inline import of attrgetter inside the function is redundant
because attrgetter is already imported at module top (from operator import
attrgetter); remove the local inline import statement (the one importing
attrgetter around the code that uses attrgetter) and ensure all uses reference
the module-level attrgetter; look for the inline import near the code that calls
attrgetter (around the function/method that performs sorting/filtering) and
delete that local import to avoid duplication.
- Around line 201-203: Move the repeated inline imports of ValidationError and
IPSet to the top of the module: add "from django.core.exceptions import
ValidationError" and "from netaddr import IPSet" to the module-level imports,
then remove the inline imports currently inside the functions/classes (the
duplicates around the uses in functions referencing ValidationError and IPSet in
rule_types.base, e.g. the blocks that import ValidationError at the two
locations and IPSet at the two locations). Ensure all references to
ValidationError and IPSet still work after removing the inline imports.
- Around line 162-163: Remove the redundant inline import of ObjectDoesNotExist
inside the function in openwisp_controller.subnet_division.rule_types.base (the
local "from django.core.exceptions import ObjectDoesNotExist" at ~line 162);
rely on the module-level import already present (imported near the top of the
file), and simply use ObjectDoesNotExist where needed in the function body
without re-importing it.
- Around line 191-193: get_max_subnet currently returns
str(candidate.previous()) relying on wrap-around behavior and subsequent .next()
in create_subnets to get back into the master range; make this explicit and less
error-prone by returning the candidate directly (not candidate.previous()) from
get_max_subnet and update create_subnets to accept that candidate as the
starting subnet and use candidate.next()/candidate as appropriate, or if you
prefer minimal change, add a clear comment in get_max_subnet explaining the
intentional previous()/next() wrap-around behavior and why validation via
subnet_of(master_subnet.subnet) will keep results in bounds; refer to
get_max_subnet, create_subnets, master_network, and the
candidate.previous()/candidate.next() usage when implementing the change.

---

Outside diff comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 116-132: The create_subnets_ips flow can leave partial DB state if
create_subnets or create_ips raise after some writes; wrap the entire
provisioning sequence in a single atomic transaction so either all created
subnets, generated_indexes (SubnetDivisionIndex.bulk_create) and ips persist or
none do. Specifically, surround the logic inside create_subnets_ips (use
transaction.atomic()) around calls to cls.create_subnets, cls.create_ips and
SubnetDivisionIndex.objects.bulk_create to ensure all-or-nothing commits and
rollbacks on exceptions.

ℹ️ Review info
Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b45a08a4-7a53-4d8a-9e72-7a8d807af4fc

📥 Commits

Reviewing files that changed from the base of the PR and between 04150aa and 00d4275.

📒 Files selected for processing (1)
  • openwisp_controller/subnet_division/rule_types/base.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~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | 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~=4.2.0
🧰 Additional context used
🧠 Learnings (3)
📚 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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.py
🔇 Additional comments (3)
openwisp_controller/subnet_division/rule_types/base.py (3)

276-294: LGTM!

The fix correctly saves each IpAddress instance before creating the corresponding SubnetDivisionIndex, ensuring the foreign key (ip_id) has a valid value. This addresses the previously identified critical FK integrity issue.


297-304: LGTM!

The destroy_provisioned_subnets_ips method correctly uses cascade deletion through the Subnet model, which will automatically clean up related IpAddress and SubnetDivisionIndex records.


223-250: Potential race condition when concurrent requests provision subnets simultaneously.

The consumed_space snapshot is computed from a single database query (lines 223-228), but another concurrent request could allocate the same subnet between this check and the save() call at line 250. Since transaction.on_commit() executes the provisioning after the triggering transaction commits, multiple threads can execute this method concurrently with no locking mechanism. If the Subnet model in openwisp_ipam lacks a unique constraint on (subnet, master_subnet), duplicate subnet allocations could occur. Even if a constraint exists, an unhandled IntegrityError would crash the operation since there is no error handling.

Copy link
Copy Markdown
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

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

Regression test missing.


for candidate in master_network.subnet(prefixlen=division_rule.size):
if not consumed_space.intersection(IPSet([candidate])):
return str(candidate.previous())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: Logic error when master subnet has no existing subnets.

When consumed_space is empty (no subnets exist in the master subnet), the first candidate will be the first available subnet (e.g., 10.0.0.0/28). Calling candidate.previous() returns a subnet outside the master subnet range, which causes create_subnets to fail when it calls IPNetwork(str(max_subnet)).next().

Fix: Return str(candidate) instead of str(candidate.previous()) when the candidate is the first available subnet.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Mar 8, 2026

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

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

CRITICAL

File Line Issue
openwisp_controller/subnet_division/rule_types/base.py 193 Logic error: candidate.previous() returns subnet outside master subnet range when no subnets exist
Files Reviewed (1 file)
  • openwisp_controller/subnet_division/rule_types/base.py - 1 critical issue

Fix these issues in Kilo Cloud

@openwisp-companion
Copy link
Copy Markdown

CI Failure Analysis

Hello @nemesifier,
(Analysis for commit 7c03e7f)

There are two distinct failures in the CI logs:

  1. Code Style/QA: The isort check failed due to incorrectly sorted or formatted imports in openwisp_controller/subnet_division/rule_types/base.py.
  2. Test Failure: Multiple tests failed, indicated by a large number of slow tests and the overall exit code of 1 from the ./runtests command. The specific tests that failed are not detailed in the provided logs, but the sheer volume of slow tests suggests potential performance issues or inefficient test setups.

Remediation

  1. Code Style/QA:

    • Explanation: The isort tool, which is part of the openwisp-qa-format script, detected issues with the import order in the file openwisp_controller/subnet_division/rule_types/base.py.
    • Remediation: Run the following command to automatically fix the import sorting and formatting:
      openwisp-qa-format
  2. Test Failure:

    • Explanation: The CI logs indicate that a significant number of tests are running slowly, contributing to the overall failure. While the specific root cause for each slow test is not detailed, this often points to inefficient test logic, redundant operations, or issues with test setup/teardown.
    • Remediation:
      • Investigate Slow Tests: Review the tests marked as slow in the CI output (if available in more detailed logs) and identify patterns. Look for tests that might be performing database queries inefficiently, making external calls, or not cleaning up resources properly.
      • Optimize Test Logic: Refactor tests to be more efficient. This might involve using select_related or prefetch_related for database queries, mocking external services, or parallelizing test execution where appropriate.
      • Review Test Setup/Teardown: Ensure that test setup and teardown procedures are efficient and do not introduce unnecessary overhead. For example, consider if test databases are being cloned or reset optimally.
      • Re-run CI: After addressing the code style issue, re-run the CI pipeline to see if the test failures are resolved. If they persist, further investigation into test performance will be necessary.

@atif09
Copy link
Copy Markdown
Contributor

atif09 commented Mar 20, 2026

ping @Piyushkhobragade

@openwisp-companion
Copy link
Copy Markdown

Code Style Failures

Hello @Piyushkhobragade,
(Analysis for commit 1b255a9)

The CI has detected several code style issues. Please run openwisp-qa-format to automatically fix most of them.

Specifically, the following files have line length issues (E501) or complexity issues (E704, E301, E302, W391, E203, F401, E402) that require manual correction:

  • yaml/tokens.py
  • yarl/_parse.py
  • yarl/_quoting_py.py
  • yarl/_url.py
  • zope/interface/__init__.py
  • zope/interface/common/collections.py
  • zope/interface/common/tests/test_collections.py
  • zope/interface/interface.py
  • zope/interface/tests/test_declarations.py
  • zope/interface/tests/test_interface.py
  • zope/interface/tests/test_registry.py
  • zope/interface/verify.py

Please address these manually and ensure all files comply with the project's coding standards.

…penwisp#864

Refactored the allocation logic to use netaddr.IPSet for mathematical
gap-filling instead of database sorting.
Added guards for exact-match /32 rules and scoped exhaustion logic
per device config to resolve test regressions.

Fixes openwisp#864
@Piyushkhobragade Piyushkhobragade force-pushed the issues/864-subnet-division-overlap branch from 1b255a9 to 511a951 Compare March 24, 2026 15:08
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

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/3992862351","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<!-- This is an auto-generated comment: review paused by coderabbit.ai -->\n\n> [!NOTE]\n> ## Reviews paused\n> \n> 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.\n> \n> Use the following commands to manage reviews:\n> - `@coderabbitai resume` to resume automatic reviews.\n> - `@coderabbitai review` to trigger a single review.\n> \n> Use the checkboxes below for quick actions:\n> - [ ] <!-- {\"checkboxId\": \"7f6cc2e2-2e4e-497a-8c31-c9e4573e93d1\"} --> ▶️ Resume reviews\n> - [ ] <!-- {\"checkboxId\": \"e9bb8d72-00e8-4f67-9cb2-caf3b22574fe\"} --> 🔍 Trigger review\n\n<!-- end of auto-generated comment: review paused by coderabbit.ai -->\n\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nRefactors subnet-division provisioning to be rule-aware and incremental. get_config retrieves the configured object robustly; get_max_subnet returns the highest existing subnet for the specific rule or creates an anchor when none exist; create_subnets incrementally generates non-overlapping subnets using an IPSet and persists each Subnet and its SubnetDivisionIndex; create_ips creates IpAddress rows per-subnet and records indexes; create_subnets_ips orchestrates the flow inside a transaction; destroy_provisioned_subnets_ips deletes subnets by rule type.\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant Rule as DivisionRuleHandler\n    participant IPTools as IPNetwork/IPSet\n    participant DB as Database(Subnet, IpAddress, SubnetDivisionIndex)\n    participant TX as Transaction\n\n    Rule->>DB: query SubnetDivisionIndex WHERE rule = X\n    DB-->>Rule: existing indexes (maybe empty)\n    alt existing subnets for rule\n        Rule->>DB: query Subnet by ids\n        DB-->>Rule: return Subnets\n        Rule->>IPTools: parse & pick max subnet\n    else no subnets for rule\n        Rule->>DB: fetch master Subnet for config\n        DB-->>Rule: return master Subnet\n        Rule->>IPTools: build candidate iterator from master_subnet\n    end\n\n    TX->>Rule: begin transaction\n    loop for each needed subnet\n        Rule->>IPTools: advance candidate until non-overlapping with used IPSet\n        Rule->>DB: create Subnet\n        DB-->>Rule: Subnet id\n        Rule->>DB: record SubnetDivisionIndex for subnet\n        loop for each IP to create in subnet\n            Rule->>DB: create IpAddress\n            DB-->>Rule: IpAddress id\n            Rule->>DB: record SubnetDivisionIndex for ip\n        end\n    end\n    Rule->>TX: commit\n    TX-->>DB: atomic commit of created rows\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~45 minutes\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n---\n\n<!-- pre_merge_checks_override_start -->\n> [!IMPORTANT]\n> ## Pre-merge checks failed\n> \n> Please resolve all errors before merging. Addressing warnings is optional.\n<!-- pre_merge_checks_override_end -->\n\n### ❌ Failed checks (1 inconclusive)\n\n| Check name | Status         | Explanation                                                                                                                                                                                                         | Resolution                                                                                                                                 |\n| :--------: | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------- |\n|  Bug Fixes | ❓ Inconclusive | Repository does not contain the PR files referenced in the summary, specifically openwisp_controller/subnet_division/rule_types/base.py and the regression test test_multiple_subnetdivision_rules_different_sizes. | Verify correct repository branch with PR changes is loaded. Ensure modified files and test files are present before re-running assessment. |\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                                                                    |\n| :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|         Title check        | ✅ Passed | The title clearly addresses the main issue (`#864`) and accurately summarizes the change: fixing overlapping subnets with different prefix sizes in subnet_division.                                             |\n|      Description check     | ✅ Passed | The description provides a clear explanation of the issue, root cause, and the fix applied. However, the PR description template sections (Checklist items, Screenshot) are not explicitly shown as completed. |\n|     Linked Issues check    | ✅ Passed | The PR directly addresses issue `#864` by fixing the overlapping subnets problem when multiple SubnetDivisionRules of different sizes are applied to the same parent subnet.                                     |\n| Out of Scope Changes check | ✅ Passed | All changes are scoped to fixing overlapping subnet allocations in the subnet_division module as required by issue `#864`. No unrelated modifications detected.                                                  |\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\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-unknown_comment_id\"} -->   Create PR with unit tests\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\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=1252)! 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<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"retries":3,"retryAfter":16}}}

Copy link
Copy Markdown
Contributor

@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

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

Inline comments:
In `@openwisp_controller/subnet_division/rule_types/base.py`:
- Around line 136-138: The current anchor detection using string containment
("Reserved" in (a_qs.first().name or "")) is fragile; modify the Subnet model to
add a dedicated boolean field (e.g., is_anchor) or a unique, unlikely name
prefix (e.g., "__anchor__") and update the lookup in the SubnetDivision logic to
check that explicit marker instead of substring-matching; specifically, add the
new field and migration on the Subnet model, set it for existing anchor subnets,
and change the check in the method that contains a_qs =
Subnet.objects.filter(subnet=max_s, master_subnet=ms) to test
a_qs.filter(is_anchor=True).exists() (or check name.startswith("__anchor__") if
you choose the prefix approach), and update any other codepaths that currently
rely on the "Reserved" name heuristic.
- Around line 147-165: The master-subnet-exhausted branch in the subnet
provisioning logic (the ip_network(...).subnet_of(ms.subnet) check and the
notify.send block in base.py) currently logs with logger.info and breaks, which
silently returns partial results; change logger.info to logger.error and instead
of break raise a descriptive exception (e.g., RuntimeError or a specific
SubnetProvisionError) that includes ms.name and ms.subnet so callers can detect
failure; keep the notify.send call but replace the silent break with the
exception to surface the error to callers.
- Around line 226-233: The base destroy_provisioned_subnets_ips currently
deletes Subnet rows by only filtering subnetdivisionindex_set by rule__type;
update it to also filter by rule__master_subnet_id using the instance context
(like VpnSubnetDivisionRuleType does) to avoid removing subnets belonging to
other master subnets. In practice, inside destroy_provisioned_subnets_ips (and
using cls.get_config(instance)), retrieve the master_subnet_id from the instance
(or config) and add rule__master_subnet_id=<that id> to the QuerySet filter
alongside rule__type so the Subnet.objects.filter(id__in=s_ids).delete() only
targets subnets for that specific master subnet. Ensure you reference
subnetdivisionindex_set, rule__type, rule__master_subnet_id, get_config, and
Subnet when making the change.
🪄 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: 47d21e6a-15b7-4ddc-be4f-5a749ba7a655

📥 Commits

Reviewing files that changed from the base of the PR and between 00d4275 and 511a951.

📒 Files selected for processing (1)
  • openwisp_controller/subnet_division/rule_types/base.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.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | 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.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | 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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.py
🧠 Learnings (3)
📚 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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.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/subnet_division/rule_types/base.py
🔇 Additional comments (6)
openwisp_controller/subnet_division/rule_types/base.py (6)

1-18: LGTM!

Imports are properly consolidated at the module level. The addition of IPSet from netaddr aligns with the PR objective to use mathematical set operations for subnet collision detection.


34-50: LGTM!

The refactored provision_receiver has cleaner control flow. The exception handling correctly catches cases where the instance doesn't have the expected configuration, and transaction.on_commit ensures provisioning happens after the transaction commits successfully.


82-88: LGTM!

The get_config method properly extracts config retrieval logic with an existence check to prevent operations on deleted configs. The implementation aligns with the subclass usage patterns seen in VpnSubnetDivisionRuleType.destroy_provisioned_subnets_ips.


166-183: LGTM!

The subnet creation correctly updates the IPSet after each allocation (line 174), ensuring subsequent iterations don't allocate overlapping space. This directly addresses the PR objective of preventing overlapping subnets.


185-209: LGTM!

The refactored create_ips correctly creates IpAddress objects via objects.create() before referencing their IDs in SubnetDivisionIndex (line 204). This addresses the previous FK relationship issue where ip_id was captured as None from unsaved instances.


104-116: The anchor creation logic correctly handles overlapping subnets with different prefix sizes through the IPSet intersection check in create_subnets. Multiple rules on the same master subnet are tested, though the test suite lacks coverage for rules with different sizes (e.g., /28 and /29 anchors simultaneously). Since overlaps are properly detected and avoided during allocation, this is not a bug but a test coverage gap if coverage for different rule sizes is desired.

Comment on lines +136 to +138
a_qs = Subnet.objects.filter(subnet=max_s, master_subnet=ms)
if exist > 0 or (a_qs.exists() and "Reserved" in (a_qs.first().name or "")):
req = req.next()
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.

🧹 Nitpick | 🔵 Trivial

Fragile string-based anchor detection.

Line 137 uses "Reserved" in (a_qs.first().name or "") to detect anchor subnets. This is fragile because:

  1. Admins could rename anchor subnets
  2. Other subnets might coincidentally contain "Reserved" in their name
  3. The string is not i18n-safe (the name uses a translated string on line 111)

Consider adding a dedicated boolean field or a specific naming convention with a unique prefix (e.g., __anchor__) that's less likely to conflict.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/subnet_division/rule_types/base.py` around lines 136 -
138, The current anchor detection using string containment ("Reserved" in
(a_qs.first().name or "")) is fragile; modify the Subnet model to add a
dedicated boolean field (e.g., is_anchor) or a unique, unlikely name prefix
(e.g., "__anchor__") and update the lookup in the SubnetDivision logic to check
that explicit marker instead of substring-matching; specifically, add the new
field and migration on the Subnet model, set it for existing anchor subnets, and
change the check in the method that contains a_qs =
Subnet.objects.filter(subnet=max_s, master_subnet=ms) to test
a_qs.filter(is_anchor=True).exists() (or check name.startswith("__anchor__") if
you choose the prefix approach), and update any other codepaths that currently
rely on the "Reserved" name heuristic.

Comment on lines +147 to 165
if not ip_network(str(req)).subnet_of(ms.subnet):
notify.send(
sender=config,
type="generic_message",
target=config.device,
action_object=master_subnet,
level="error",
action_object=ms,
message=_(
"Failed to provision subnets for"
" [{notification.target}]({notification.target_link})"
"Failed to provision subnets for "
"[{notification.target}]({notification.target_link})"
),
description=_(
"The [{notification.action_object}]({notification.action_link})"
" subnet has run out of space."
"The [{notification.action_object}]"
"({notification.action_link}) subnet has run "
"out of space."
),
)
logger.info(f"Cannot create more subnets of {master_subnet}")
logger.info(f"Cannot create more subnets of {ms.name} {ms.subnet}")
break
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.

⚠️ Potential issue | 🟡 Minor

Use appropriate log level for error condition and consider raising an exception.

Line 164 uses logger.info for a failure condition (master subnet exhausted), but per coding guidelines, errors that cannot be resolved by the user should use logger.error. Additionally, the method silently returns partial results after break instead of signaling failure to the caller.

🔧 Proposed fix
-                logger.info(f"Cannot create more subnets of {ms.name} {ms.subnet}")
+                logger.error(
+                    f"Cannot create more subnets of {ms.name} ({ms.subnet}): "
+                    f"address space exhausted after {len(subs)} of "
+                    f"{rule.number_of_subnets} requested subnets."
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/subnet_division/rule_types/base.py` around lines 147 -
165, The master-subnet-exhausted branch in the subnet provisioning logic (the
ip_network(...).subnet_of(ms.subnet) check and the notify.send block in base.py)
currently logs with logger.info and breaks, which silently returns partial
results; change logger.info to logger.error and instead of break raise a
descriptive exception (e.g., RuntimeError or a specific SubnetProvisionError)
that includes ms.name and ms.subnet so callers can detect failure; keep the
notify.send call but replace the silent break with the exception to surface the
error to callers.

Comment on lines 226 to +233
@classmethod
def destroy_provisioned_subnets_ips(cls, instance, **kwargs):
# Deleting related subnets automatically deletes related IpAddress
# and SubnetDivisionIndex objects
config = cls.get_config(instance)
rule_type = f"{cls.__module__}.{cls.__name__}"
subnet_ids = config.subnetdivisionindex_set.filter(
rule__type=rule_type
).values_list("subnet_id")
Subnet.objects.filter(id__in=subnet_ids).delete()
rt = f"{cls.__module__}.{cls.__name__}"
s_ids = config.subnetdivisionindex_set.filter(rule__type=rt).values_list(
"subnet_id", flat=True
)
Subnet.objects.filter(id__in=s_ids).delete()
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if a rule type can be applied to multiple master subnets
rg -n "master_subnet" openwisp_controller/subnet_division/base/models.py -A 3 -B 3

# Check how rules are typically associated
rg -n "subnetdivisionrule_set" openwisp_controller/subnet_division/ --type py -A 2 -B 2

Repository: openwisp/openwisp-controller

Length of output: 4359


🏁 Script executed:

# Find the VpnSubnetDivisionRuleType class and its destroy method
fd "vpn.py" openwisp_controller/subnet_division/ --type f | head -5

Repository: openwisp/openwisp-controller

Length of output: 125


🏁 Script executed:

# View the destroy_provisioned_subnets_ips method in vpn.py
rg -n "destroy_provisioned_subnets_ips" openwisp_controller/subnet_division/rule_types/vpn.py -A 8 -B 2

Repository: openwisp/openwisp-controller

Length of output: 635


🏁 Script executed:

# Check the base method and how it's called
rg -n "destroy_provisioned_subnets_ips" openwisp_controller/subnet_division/rule_types/base.py -A 8 -B 2

Repository: openwisp/openwisp-controller

Length of output: 846


🏁 Script executed:

# Check if rules are looked up by both type and master_subnet together
rg -n "filter.*rule__type" openwisp_controller/subnet_division/ --type py -B 3 -A 3

Repository: openwisp/openwisp-controller

Length of output: 820


🏁 Script executed:

# Find all classes that inherit from SubnetDivisionRuleType
rg -n "class.*SubnetDivisionRuleType" openwisp_controller/subnet_division/ --type py

Repository: openwisp/openwisp-controller

Length of output: 783


🏁 Script executed:

# Check if destroy_provisioned_subnets_ips is overridden in other subclasses
rg -n "class.*SubnetDivisionRuleType|def destroy_provisioned_subnets_ips" openwisp_controller/subnet_division/rule_types/ --type py

Repository: openwisp/openwisp-controller

Length of output: 899


🏁 Script executed:

# Check how the destroy method is called - via signal handlers or explicit calls
rg -n "destroy_provisioned_subnets_ips|destroyer_receiver" openwisp_controller/subnet_division/ --type py -B 2 -A 2

Repository: openwisp/openwisp-controller

Length of output: 2507


🏁 Script executed:

# Check the full implementation of DeviceSubnetDivisionRuleType.destroy_provisioned_subnets_ips
sed -n '37,50p' openwisp_controller/subnet_division/rule_types/device.py

Repository: openwisp/openwisp-controller

Length of output: 672


🏁 Script executed:

# Check if there's context about how different rule types are instantiated
rg -n "rule_class|get_rule_class" openwisp_controller/subnet_division/ --type py -A 2 -B 2

Repository: openwisp/openwisp-controller

Length of output: 2519


🏁 Script executed:

# Understand the relationship between Config and SubnetDivisionRule
rg -n "config\.subnetdivisionindex_set" openwisp_controller/ --type py -B 3 -A 3

Repository: openwisp/openwisp-controller

Length of output: 6177


🏁 Script executed:

# Check SUBNET_DIVISION_TYPES setting to see which rule types are registered
rg -n "SUBNET_DIVISION_TYPES" openwisp_controller/ --type py -A 5 -B 2

Repository: openwisp/openwisp-controller

Length of output: 3186


🏁 Script executed:

# Check if BaseSubnetDivisionRuleType is ever instantiated as a rule type
rg -n "BaseSubnetDivisionRuleType" openwisp_controller/ --type py | grep -v "^.*:(class|import|@)"

Repository: openwisp/openwisp-controller

Length of output: 1253


🏁 Script executed:

# Look for any other custom subclasses of BaseSubnetDivisionRuleType outside of rule_types/
fd --type f "\.py$" openwisp_controller/ | xargs grep -l "BaseSubnetDivisionRuleType" | grep -v rule_types

Repository: openwisp/openwisp-controller

Length of output: 126


🏁 Script executed:

# Check if there's any documentation about destroy_provisioned_subnets_ips behavior
rg -n "destroy_provisioned_subnets_ips|master_subnet" openwisp_controller/subnet_division/rule_types/base.py -B 5 -A 10 | head -50

Repository: openwisp/openwisp-controller

Length of output: 2093


🏁 Script executed:

# Check the SubnetDivisionIndex model to understand the data model better
rg -n "class SubnetDivisionIndex" openwisp_controller/subnet_division/ --type py -A 15

Repository: openwisp/openwisp-controller

Length of output: 478


🏁 Script executed:

# Check the AbstractSubnetDivisionIndex model structure
rg -n "class AbstractSubnetDivisionIndex" openwisp_controller/subnet_division/ --type py -A 20

Repository: openwisp/openwisp-controller

Length of output: 1907


Add master_subnet filtering to the base destroy method for consistency and correctness.

The base destroy_provisioned_subnets_ips filters only by rule__type, but VpnSubnetDivisionRuleType overrides it to also filter by rule__master_subnet_id. Since the unique constraint allows the same rule type across multiple master subnets, the base implementation could delete subnets from other master subnets if a custom rule type inherits without overriding this method. The query should filter by both rule__type and rule__master_subnet_id (obtained from the instance context, similar to how VpnSubnetDivisionRuleType does it) to ensure only the intended subnets are deleted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/subnet_division/rule_types/base.py` around lines 226 -
233, The base destroy_provisioned_subnets_ips currently deletes Subnet rows by
only filtering subnetdivisionindex_set by rule__type; update it to also filter
by rule__master_subnet_id using the instance context (like
VpnSubnetDivisionRuleType does) to avoid removing subnets belonging to other
master subnets. In practice, inside destroy_provisioned_subnets_ips (and using
cls.get_config(instance)), retrieve the master_subnet_id from the instance (or
config) and add rule__master_subnet_id=<that id> to the QuerySet filter
alongside rule__type so the Subnet.objects.filter(id__in=s_ids).delete() only
targets subnets for that specific master subnet. Ensure you reference
subnetdivisionindex_set, rule__type, rule__master_subnet_id, get_config, and
Subnet when making the change.

@openwisp-companion
Copy link
Copy Markdown

CI Failures: Code Style and Commit Message

Hello @Piyushkhobragade,
(Analysis for commit 511a951)

  1. Code Style/QA: The CI failed due to incorrectly sorted or formatted imports in openwisp_controller/subnet_division/rule_types/base.py. Please run openwisp-qa-format to fix this.
  2. Commit Message: The commit message does not follow the required format. Please ensure your commit messages adhere to the following structure:
[tag] Capitalized short title #<issue>

<description>

For example:

[feature] Add new user authentication method #123

Implemented a new authentication method using OAuth2, enhancing security
and user experience.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Multiple SubnetDivisionRule of different sizes

3 participants