Skip to content

fix(network): Relax Constraints for Resizing Dynamic Range Reservations LP: #2143090#280

Open
bryanfraschetti wants to merge 6 commits into
canonical:masterfrom
bryanfraschetti:fix_dynamic_range_resizing
Open

fix(network): Relax Constraints for Resizing Dynamic Range Reservations LP: #2143090#280
bryanfraschetti wants to merge 6 commits into
canonical:masterfrom
bryanfraschetti:fix_dynamic_range_resizing

Conversation

@bryanfraschetti

@bryanfraschetti bryanfraschetti commented May 27, 2026

Copy link
Copy Markdown

When an IP is allocated in a dynamic range reservation, the resize operation validator views the range as discontinuous and instead sees multiple contiguous blocks separated by each allocated IP. As a result, shrinking and expanding the reservation are both rejected with "Requested dynamic range conflicts with an existing IP address or range" despite the requested action being otherwise sensible

This commit addresses the issue by making sure the new boundaries are contained by an unused range

Resolves LP:2143090

@bryanfraschetti bryanfraschetti requested a review from a team May 27, 2026 11:30
@maas-lander

Copy link
Copy Markdown
Collaborator

Check where you would like a Mattermost message to be sent to when CI completes and this PR is merged

  • Direct message
  • ~maas

@bryanfraschetti bryanfraschetti force-pushed the fix_dynamic_range_resizing branch 2 times, most recently from 735e40e to fc872ef Compare May 27, 2026 13:15
r00ta

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes validation for resizing existing dynamic IP ranges when there are already allocated IPs inside the current range, which previously caused the “Requested dynamic range conflicts…” error by treating the range as discontinuous.

Changes:

  • Updates dynamic range overlap validation to permit resizing when only the newly-added segments are within unused space.
  • Adds regression tests ensuring an existing dynamic range can shrink/expand even with an allocated IP inside the original range.

Reviewed changes

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

File Description
src/maasserver/models/iprange.py Adjusts overlap/duplicate validation to allow certain dynamic-range resizes with in-range allocations.
src/maasserver/models/tests/test_iprange.py Adds tests covering shrink/expand of an existing dynamic range when an IP inside it is allocated.

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

Comment thread src/maasserver/models/iprange.py Outdated
Comment on lines +722 to +760
def test_modify_existing_dynamic_range_can_shrink_with_allocated_ip(self):
subnet = make_plain_subnet()
iprange = IPRange(
subnet=subnet,
type=IPRANGE_TYPE.DYNAMIC,
start_ip="192.168.0.10",
end_ip="192.168.0.50",
)
iprange.save()

factory.make_StaticIPAddress(
subnet=subnet,
alloc_type=IPADDRESS_TYPE.AUTO,
ip="192.168.0.30",
)

iprange.end_ip = "192.168.0.49"
iprange.clean()
iprange.save()

def test_modify_existing_dynamic_range_can_expand_with_allocated_ip(self):
subnet = make_plain_subnet()
iprange = IPRange(
subnet=subnet,
type=IPRANGE_TYPE.DYNAMIC,
start_ip="192.168.0.10",
end_ip="192.168.0.50",
)
iprange.save()

factory.make_StaticIPAddress(
subnet=subnet,
alloc_type=IPADDRESS_TYPE.AUTO,
ip="192.168.0.30",
)

iprange.end_ip = "192.168.0.60"
iprange.clean()
iprange.save()
@tanzin8r

tanzin8r commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the fix.

Implementation

  • Consider caching the original start, end, and subnet when the model is loaded from the database using from_db, so validation does not need an extra query on every clean().
  • The same overlap check exists in the v3 ipranges API. I can help with that part if you want.
  • When the range is moved to another subnet, the resize exception should not apply. Use full validation on the new subnet instead.

Testing

I ran additional cases on top of the PR tests, including the subnet-change guard.

Expected to pass on resize:

  1. Shrink end with lease inside pool
    Range 10–50, lease at .30, end changed to .49.
    Expected: clean() and save() succeed. Only addresses at the tail are removed; nothing new is claimed.

  2. Expand end with lease inside pool
    Range 10–50, lease at .30, end changed to .60.
    Expected: succeed. Only 51–60 is new and should be free. The lease inside the old range should not block this.
    I also checked on master without the fix: this fails with error from the bug report. With the PR it passes.

  3. Shrink start with lease inside pool
    Range 10–50, lease at .30, start changed to .15.
    Expected: succeed. Same idea as shrinking the end, but on the start boundary.

  4. Expand start into free space on the left
    Range 20–50, lease at .30, start changed to .10 with free space below 20.
    Expected: succeed. Only the new strip on the left must be free.

  5. Shrink end so lease ends up outside pool
    Range 10–50, lease at .30, end changed to .25.
    Expected: succeed. The lease is no longer inside the range after the change.

All of the above passed.

Expected to fail on resize:

  1. Expand end over a lease outside the old pool
    Range 10–20, lease at .25, end changed to .30.
    Expected: rejected. The new strip 21–30 includes an address that is already in use.

  2. Expand start over a lease outside the old pool
    Range 20–50, lease at .15, start changed to .10.
    Expected: rejected. The new strip 10–19 includes an address that is already in use.

  3. Move pool to another subnet over a busy IP
    Range moved to another subnet where the new bounds overlap a busy address.
    Expected: rejected. The resize exception should not apply; validation should run on the new subnet.

  4. Create new pool spanning an already-assigned IP
    New dynamic range 10–50 while .30 is already assigned on the subnet.
    Expected: rejected. The fix is for editing an existing range, not for creating one over a split gap.

All of the above were rejected as expected.

@tanzin8r

tanzin8r commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Nice work on the caching and subnet guard! Could you rebase onto upstream master when you get a chance? Some of the tests were failing are fixed there now.
On tests, I think you have the main bug covered already. The ones I would still add are the failure cases: expanding over a busy IP just outside the old pool on both start and end, a full clean() test when moving the range to another subnet that already has a clash, and one create-path test to make sure new ranges still get rejected if they span an already-assigned IP.

On v3 side, the logic lives in src/maasapiserver/v3/api/public/models/requests/ipranges.py inside to_builder(), called from update_fabric_vlan_subnet_iprange() in the handler. Subnet move is less of an issue there since the URL pins the subnet, but the resize-with-lease-inside-pool bug will still show up. We can create a seperate PR for that.

@bryanfraschetti bryanfraschetti force-pushed the fix_dynamic_range_resizing branch from 22860a9 to 34b548d Compare June 10, 2026 12:20
@bryanfraschetti

bryanfraschetti commented Jun 10, 2026

Copy link
Copy Markdown
Author

Thanks for the review!

Good idea to add tests for the failure cases. I just rebased and added some tests to cover the ones you mentioned: resizing, moving, or creating dynamic ranges over a busy IP.

I'll look into the v3 API and put up changes in a separate PR. Thank you for the pointers regarding that

…ns LP: #2143090

When an IP is allocated in a dynamic range reservation, the resize operation
validator views the range as discontinuous and instead sees multiple contiguous
blocks separated by each allocated IP. As a result, shrinking and expanding the
reservation are both rejected with "Requested dynamic range conflicts with an
existing IP address or range" despite the requested action being otherwise
sensible (LP: #2143090)

This commit addresses the issue by making sure the new boundaries are contained
by an unused range
…ging Subnets

Add tests to cover the expected failure of a resize over a busy IP, ensure that
moving a dynamic range to a new subnet with a busy IP fails validation, and
finally ensure that creation of a range over a busy IP is also disallowed
@bryanfraschetti bryanfraschetti force-pushed the fix_dynamic_range_resizing branch from d1d5d25 to 638b2fa Compare June 17, 2026 15:33
@tanzin8r

tanzin8r commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

lgtm. Thank you for the edits. Just one last test to make this concrete.

    def test_modify_existing_dynamic_range_can_expand_start_with_allocated_ip(
        self,
    ):
        subnet = make_plain_subnet()
        iprange = IPRange(
            subnet=subnet,
            type=IPRANGE_TYPE.DYNAMIC,
            start_ip="192.168.0.20",
            end_ip="192.168.0.50",
        )
        iprange.save()
        factory.make_StaticIPAddress(
            subnet=subnet,
            alloc_type=IPADDRESS_TYPE.AUTO,
            ip="192.168.0.30",
        )
        iprange.start_ip = "192.168.0.10"
        iprange.clean()
        iprange.save()

@tanzin8r tanzin8r left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

@r00ta r00ta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

questions inline


self._raise_validation_error(message)

def _is_existing_dynamic_range_resize_allowed(

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.

why is this logic specific only for dynamic ranges? what about the other reserved ranges?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The other types of reserved ranges use different validation logic. This is specified in L255 in src/maasserver/models/iprange.py

# Reserved ranges can overlap allocated IPs but not other ranges.
# Dynamic ranges cannot overlap anything (no ranges or IPs).

So Reserved ranges can overlap IPs and only hit a conflict when the range is overlapping another range. Because of that, resizing a reserved range is unaffected by allocations inside it. As a result when an IP address within a Reserved range was allocated, it would not affect the validation of the operation.

However, for dynamic range reservations, a resize should also be allowed in this case (because the IP allocation belongs to the existing reservation). However, the validation logic was incorrect and would reject such a resize because it viewed the allocated IP as splitting a continuous block of IPs and thought there was no way to modify the range reservation without overlapping an IP, which is not allowed.

shrinking (no added segments) or if all newly added segments fit
within available unused ranges. Returns False if the original range
was not dynamic, the range moved to a different subnet, or any added
segment would overlap allocated IPs or other ranges.

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.

would overlap allocated IPs

and where would this logic be in this function?

@bryanfraschetti bryanfraschetti Jun 25, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In _validate_duplicates_and_overlaps the unused ranges are computed in this line:

            unused = self.subnet.get_ipranges_available_for_dynamic_range(
                exclude_ip_range_id=self.id
            )

That unused value is passed to this helper function and in this function we look at the proposed new range reservation boundaries and ensure that both are in unused ranges. Any extension to the left (eg. decreasing the lower bound of the range) must be in the same unused range as the current lower bound. Similar story for expansions to the right (increasing the upper bound of the reservation). If there is another allocated IP, the get_ipranges_available_for_dynamic_range will have split the available ranges on that IP, and then the following code block will return False (rejecting any proposed resize that would overlap an allocated IP) since the proposed boundary would not be in the same unused range as the current one

            if not any(
                added_start_ip in unused_range and added_end_ip in unused_range
                for unused_range in unused
            ):
                return False

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.

5 participants