Prefix Available IPs behaviour changed #11484

Closed
opened 2025-12-29 21:45:53 +01:00 by adam · 4 comments
Owner

Originally created by @yarnocobussen-sbp on GitHub (Aug 14, 2025).

Deployment Type

Self-hosted

NetBox Version

v4.3.5

Python Version

3.12

Steps to Reproduce

  1. Create a prefix 10.0.0.0/24
  2. Create an ip-range 10.0.0.1-9/24
  3. POST [{"status": "active"}] to /api/ipam/prefixes/{id}/available-ips/

Expected Behavior

An IP Address 10.0.0.10/24 will be created.

Observed Behavior

An IP Address 10.0.0.1/24 is created.

Available IP Addresses within IP Ranges used to not be considered when calling /api/ipam/prefixes/{id}/available-ips/.
My last test for the expected behavior was in Netbox v4.1.11.
You had to explicitly call /api/ipam/ip-ranges/{id}/available-ips/ to create an address inside an IP Range.

I cannot find anything a mention of this in patch notes or issues, but I believe the changes for the fully utilized property on IP Ranges may have influenced this behaviour. (Please point me to an issue if I've missed something)

Reproducing the old behavior for /api/ipam/prefixes/{id}/available-ips/. seems to only be possible now by setting fully utilised to true for each IP Range within the Prefix, but that also causes you to be unable to now provision addresses into said IP Ranges.

The workaround is to first lookup IP Ranges for a Prefix, set them all to fully utilized, provision your IP Address, and then revert the changes to the IP Ranges. However, that's a bit strange and leads to change logs on IP Range objects every time you want to provision an IP Address.

Is there anything I'm missing? Is this intended behaviour or not? It breaks our provisioning logic.

Originally created by @yarnocobussen-sbp on GitHub (Aug 14, 2025). ### Deployment Type Self-hosted ### NetBox Version v4.3.5 ### Python Version 3.12 ### Steps to Reproduce 1. Create a prefix 10.0.0.0/24 2. Create an ip-range 10.0.0.1-9/24 3. POST [{"status": "active"}] to /api/ipam/prefixes/{id}/available-ips/ ### Expected Behavior An IP Address 10.0.0.10/24 will be created. ### Observed Behavior An IP Address 10.0.0.1/24 is created. Available IP Addresses within IP Ranges used to not be considered when calling /api/ipam/prefixes/{id}/available-ips/. My last test for the expected behavior was in Netbox v4.1.11. You had to explicitly call /api/ipam/ip-ranges/{id}/available-ips/ to create an address inside an IP Range. I cannot find anything a mention of this in patch notes or issues, but I believe the changes for the fully utilized property on IP Ranges may have influenced this behaviour. (Please point me to an issue if I've missed something) Reproducing the old behavior for /api/ipam/prefixes/{id}/available-ips/. seems to only be possible now by setting fully utilised to true for each IP Range within the Prefix, but that also causes you to be unable to now provision addresses into said IP Ranges. The workaround is to first lookup IP Ranges for a Prefix, set them all to fully utilized, provision your IP Address, and then revert the changes to the IP Ranges. However, that's a bit strange and leads to change logs on IP Range objects every time you want to provision an IP Address. Is there anything I'm missing? Is this intended behaviour or not? It breaks our provisioning logic.
adam added the netbox label 2025-12-29 21:45:53 +01:00
adam closed this issue 2025-12-29 21:45:53 +01:00
Author
Owner

@bctiemann commented on GitHub (Aug 15, 2025):

Setting to Needs Owner so this behavior can be investigated. It would be ideal if the version where this changed could be pinpointed; that will make it much easier to determine whether this is intended behavior or an inadvertent side effect of another change (my suspicion is the latter).

@bctiemann commented on GitHub (Aug 15, 2025): Setting to Needs Owner so this behavior can be investigated. It would be ideal if the version where this changed could be pinpointed; that will make it much easier to determine whether this is intended behavior or an inadvertent side effect of another change (my suspicion is the latter).
Author
Owner

@yarnocobussen-sbp commented on GitHub (Aug 15, 2025):

It seems a lot of work related to this was done here for the 4.3.0 release:
https://github.com/netbox-community/netbox/pull/19064

Looking at the changes, I have a feeling that it has to do with how mark_populated now influences which addresses get returned when checking which addresses are available within a given prefix:

 def get_available_ips(self):
    """
    Return all available IPs within this prefix as an IPSet.
    """
    prefix = netaddr.IPSet(self.prefix)
    child_ips = netaddr.IPSet([
        ip.address.ip for ip in self.get_child_ips()
    ])
    child_ranges = netaddr.IPSet([
        iprange.range for iprange in self.get_child_ranges().filter(mark_populated=True)
    ])
    available_ips = prefix - child_ips - child_ranges

    # IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable
    if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (
            self.family == 4 and self.prefix.prefixlen >= 31
    ):
        return available_ips

    if self.family == 4:
        # For "normal" IPv4 prefixes, omit first and last addresses
        available_ips -= netaddr.IPSet([
            netaddr.IPAddress(self.prefix.first),
            netaddr.IPAddress(self.prefix.last),
        ])
    else:
        # For IPv6 prefixes, omit the Subnet-Router anycast address
        # per RFC 4291
        available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)])

    return available_ips

It no longer cares if an address belongs to a range, unless mark_populated=True.
The old behaviour removed the child ranges from the available addresses, as far as I can tell:

 def get_available_ips(self):
    """
    Return all available IPs within this prefix as an IPSet.
    """
    if self.mark_utilized:
        return netaddr.IPSet()

    prefix = netaddr.IPSet(self.prefix)
    child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
    child_ranges = []
    for iprange in self.get_child_ranges():
        child_ranges.append(iprange.range)
    available_ips = prefix - child_ips - netaddr.IPSet(child_ranges)

    # IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable
    if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31):
        return available_ips

    if self.family == 4:
        # For "normal" IPv4 prefixes, omit first and last addresses
        available_ips -= netaddr.IPSet([
            netaddr.IPAddress(self.prefix.first),
            netaddr.IPAddress(self.prefix.last),
        ])
    else:
        # For IPv6 prefixes, omit the Subnet-Router anycast address
        # per RFC 4291
        available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)])
    return available_ips

I don't know if this change was intended or not, but the discussions regarding this change and the patch notes don't mention it, so I suspect it's an unintended side effect of the change.

@yarnocobussen-sbp commented on GitHub (Aug 15, 2025): It seems a lot of work related to this was done here for the 4.3.0 release: https://github.com/netbox-community/netbox/pull/19064 Looking at the changes, I have a feeling that it has to do with how mark_populated now influences which addresses get returned when checking which addresses are available within a given prefix: def get_available_ips(self): """ Return all available IPs within this prefix as an IPSet. """ prefix = netaddr.IPSet(self.prefix) child_ips = netaddr.IPSet([ ip.address.ip for ip in self.get_child_ips() ]) child_ranges = netaddr.IPSet([ iprange.range for iprange in self.get_child_ranges().filter(mark_populated=True) ]) available_ips = prefix - child_ips - child_ranges # IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or ( self.family == 4 and self.prefix.prefixlen >= 31 ): return available_ips if self.family == 4: # For "normal" IPv4 prefixes, omit first and last addresses available_ips -= netaddr.IPSet([ netaddr.IPAddress(self.prefix.first), netaddr.IPAddress(self.prefix.last), ]) else: # For IPv6 prefixes, omit the Subnet-Router anycast address # per RFC 4291 available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)]) return available_ips It no longer cares if an address belongs to a range, unless mark_populated=True. The old behaviour removed the child ranges from the available addresses, as far as I can tell: def get_available_ips(self): """ Return all available IPs within this prefix as an IPSet. """ if self.mark_utilized: return netaddr.IPSet() prefix = netaddr.IPSet(self.prefix) child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]) child_ranges = [] for iprange in self.get_child_ranges(): child_ranges.append(iprange.range) available_ips = prefix - child_ips - netaddr.IPSet(child_ranges) # IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (self.family == 4 and self.prefix.prefixlen >= 31): return available_ips if self.family == 4: # For "normal" IPv4 prefixes, omit first and last addresses available_ips -= netaddr.IPSet([ netaddr.IPAddress(self.prefix.first), netaddr.IPAddress(self.prefix.last), ]) else: # For IPv6 prefixes, omit the Subnet-Router anycast address # per RFC 4291 available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)]) return available_ips I don't know if this change was intended or not, but the discussions regarding this change and the patch notes don't mention it, so I suspect it's an unintended side effect of the change.
Author
Owner

@jeremystretch commented on GitHub (Aug 20, 2025):

NetBox v4.3 introduced the ability to mark IP ranges as populated (see #9763). By default, IP ranges are not marked as populated, so the behavior described above is expected. If you mark the range as populated, its space will no longer be considered available.

@jeremystretch commented on GitHub (Aug 20, 2025): NetBox v4.3 introduced the ability to mark IP ranges as populated (see #9763). By default, IP ranges are not marked as populated, so the behavior described above is expected. If you mark the range as populated, its space will no longer be considered available.
Author
Owner

@yarnocobussen-sbp commented on GitHub (Aug 20, 2025):

@jeremystretch
I understand the new feature and why it does this.
What I'm saying is that invalidated existing workflows.

Take the following situation. You want to:

  • manage all ips within 10.0.0.0/24, a child range 10.0.0.1-9/24 and a child range 10.0.0.100-199/24
  • use the available-ips api for the prefix to get any ip except for from the ranges

That now requires either:

  • getting, 'locking', using prefix available-ips and then 'unlocking' all child ranges each time
  • creating multiple dummy ranges that you loop over, and no longer using prefix available-ips
  • creating your own available-ips function

They all seem like poor solutions.

If there was a way to request .filter(mark_populated=True) to not be applied when calling available-ips, that would re-enable the use case. Would a feature request for such a thing get rejected?

@yarnocobussen-sbp commented on GitHub (Aug 20, 2025): @jeremystretch I understand the new feature and why it does this. What I'm saying is that invalidated existing workflows. Take the following situation. You want to: - manage all ips within 10.0.0.0/24, a child range 10.0.0.1-9/24 and a child range 10.0.0.100-199/24 - use the available-ips api for the prefix to get any ip except for from the ranges That now requires either: - getting, 'locking', using prefix available-ips and then 'unlocking' all child ranges each time - creating multiple dummy ranges that you loop over, and no longer using prefix available-ips - creating your own available-ips function They all seem like poor solutions. If there was a way to request .filter(mark_populated=True) to not be applied when calling available-ips, that would re-enable the use case. Would a feature request for such a thing get rejected?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#11484