mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
parent
076d16ca6b
commit
f8f2ad1d14
@ -2,6 +2,12 @@
|
||||
|
||||
This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the `size` property on an IPRange instance.) Like [prefixes](./prefix.md) and [IP addresses](./ipaddress.md), each IP range may optionally be assigned to a [VRF](./vrf.md).
|
||||
|
||||
Each IP range can be marked as populated, which instructs NetBox to treat the range as though every IP address within it has been created (even though these individual IP addresses don't actually exist in the database). This can be helpful in scenarios where the management of a subset of IP addresses has been deferred to an external system of record, such as a DHCP server. NetBox will prohibit the creation of individual IP addresses within a range that has been marked as populated.
|
||||
|
||||
An IP range can also be marked as utilized. This will cause its utilization to always be reported as 100% when viewing the range or when calculating the utilization of a parent prefix. (If not enabled, a range's utilization is calculated based on the number of IP addresses which have been created within it.)
|
||||
|
||||
Typically, IP ranges marked as populated should also be marked as utilized, although there may be scenarios where this is undesirable (e.g. when reclaiming old IP space). An IP range which has been marked as populated but _not_ marked as utilized will always report a utilization of 0%, as it cannot contain child IP addresses.
|
||||
|
||||
## Fields
|
||||
|
||||
### VRF
|
||||
@ -29,6 +35,12 @@ The IP range's operational status. Note that the status of a range does _not_ ha
|
||||
!!! tip
|
||||
Additional statuses may be defined by setting `IPRange.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||
|
||||
### Mark Populated
|
||||
|
||||
!!! note "This field was added in NetBox v4.3."
|
||||
|
||||
If enabled, NetBox will treat this IP range as being fully populated when calculating available IP space. It will also prevent the creation of IP addresses which fall within the declared range (and assigned VRF, if any).
|
||||
|
||||
### Mark Utilized
|
||||
|
||||
If enabled, the IP range will be considered 100% utilized regardless of how many IP addresses are defined within it. This is useful for documenting DHCP ranges, for example.
|
||||
|
@ -147,7 +147,8 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
|
||||
'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_populated', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
|
||||
|
||||
|
@ -478,7 +478,7 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = ('id', 'mark_utilized', 'size', 'description')
|
||||
fields = ('id', 'mark_populated', 'mark_utilized', 'size', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -296,6 +296,11 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
queryset=Role.objects.all(),
|
||||
required=False
|
||||
)
|
||||
mark_populated = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label=_('Treat as populated')
|
||||
)
|
||||
mark_utilized = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
|
@ -268,8 +268,8 @@ class IPRangeImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = (
|
||||
'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'mark_utilized', 'description',
|
||||
'comments', 'tags',
|
||||
'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'mark_populated', 'mark_utilized',
|
||||
'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
|
@ -266,7 +266,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = IPRange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_utilized', name=_('Attributes')),
|
||||
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
family = forms.ChoiceField(
|
||||
@ -291,6 +291,13 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
null_option='None',
|
||||
label=_('Role')
|
||||
)
|
||||
mark_populated = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Treat as populated'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
mark_utilized = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Treat as fully utilized'),
|
||||
|
@ -257,8 +257,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags',
|
||||
name=_('IP Range')
|
||||
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description',
|
||||
'tags', name=_('IP Range')
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@ -266,8 +266,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_utilized',
|
||||
'description', 'comments', 'tags',
|
||||
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
|
||||
'mark_utilized', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
16
netbox/ipam/migrations/0078_iprange_mark_utilized.py
Normal file
16
netbox/ipam/migrations/0078_iprange_mark_utilized.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0077_vlangroup_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='iprange',
|
||||
name='mark_populated',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
@ -383,14 +383,15 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
else:
|
||||
return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
|
||||
|
||||
def get_child_ranges(self):
|
||||
def get_child_ranges(self, **kwargs):
|
||||
"""
|
||||
Return all IPRanges within this Prefix and VRF.
|
||||
"""
|
||||
return IPRange.objects.filter(
|
||||
vrf=self.vrf,
|
||||
start_address__net_host_contained=str(self.prefix),
|
||||
end_address__net_host_contained=str(self.prefix)
|
||||
end_address__net_host_contained=str(self.prefix),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_child_ips(self):
|
||||
@ -407,15 +408,14 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
"""
|
||||
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)
|
||||
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 (
|
||||
@ -433,6 +433,7 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
# For IPv6 prefixes, omit the Subnet-Router anycast address
|
||||
# per RFC 4291
|
||||
available_ips -= netaddr.IPSet([netaddr.IPAddress(self.prefix.first)])
|
||||
|
||||
return available_ips
|
||||
|
||||
def get_first_available_ip(self):
|
||||
@ -461,9 +462,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
utilization = float(child_prefixes.size) / self.prefix.size * 100
|
||||
else:
|
||||
# Compile an IPSet to avoid counting duplicate IPs
|
||||
child_ips = netaddr.IPSet(
|
||||
[_.range for _ in self.get_child_ranges()] + [_.address.ip for _ in self.get_child_ips()]
|
||||
)
|
||||
child_ips = netaddr.IPSet()
|
||||
for iprange in self.get_child_ranges().filter(mark_utilized=True):
|
||||
child_ips.add(iprange.range)
|
||||
for ip in self.get_child_ips():
|
||||
child_ips.add(ip.address.ip)
|
||||
|
||||
prefix_size = self.prefix.size
|
||||
if self.prefix.version == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
|
||||
@ -519,14 +522,19 @@ class IPRange(ContactsMixin, PrimaryModel):
|
||||
null=True,
|
||||
help_text=_('The primary function of this range')
|
||||
)
|
||||
mark_populated = models.BooleanField(
|
||||
verbose_name=_('mark populated'),
|
||||
default=False,
|
||||
help_text=_("Prevent the creation of IP addresses within this range")
|
||||
)
|
||||
mark_utilized = models.BooleanField(
|
||||
verbose_name=_('mark utilized'),
|
||||
default=False,
|
||||
help_text=_("Treat as fully utilized")
|
||||
help_text=_("Report space as 100% utilized")
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'vrf', 'tenant', 'status', 'role', 'description',
|
||||
'vrf', 'tenant', 'status', 'role', 'description', 'mark_populated', 'mark_utilized',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -663,6 +671,9 @@ class IPRange(ContactsMixin, PrimaryModel):
|
||||
"""
|
||||
Return all available IPs within this range as an IPSet.
|
||||
"""
|
||||
if self.mark_populated:
|
||||
return netaddr.IPSet()
|
||||
|
||||
range = netaddr.IPRange(self.start_address.ip, self.end_address.ip)
|
||||
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
|
||||
|
||||
@ -875,6 +886,20 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
)
|
||||
})
|
||||
|
||||
# Disallow the creation of IPAddresses within an IPRange with mark_populated=True
|
||||
parent_range = IPRange.objects.filter(
|
||||
start_address__lte=self.address,
|
||||
end_address__gte=self.address,
|
||||
vrf=self.vrf,
|
||||
mark_populated=True
|
||||
).first()
|
||||
if parent_range:
|
||||
raise ValidationError({
|
||||
'address': _(
|
||||
"Cannot create IP address {ip} inside range {range}."
|
||||
).format(ip=self.address, range=parent_range)
|
||||
})
|
||||
|
||||
if self._original_assigned_object_id and self._original_assigned_object_type_id:
|
||||
parent = getattr(self.assigned_object, 'parent_object', None)
|
||||
ct = ObjectType.objects.get_for_id(self._original_assigned_object_type_id)
|
||||
|
@ -10,6 +10,7 @@ from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
'AggregateTable',
|
||||
'AnnotatedIPAddressTable',
|
||||
'AssignedIPAddressesTable',
|
||||
'IPAddressAssignTable',
|
||||
'IPAddressTable',
|
||||
@ -268,6 +269,10 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name=_('Role'),
|
||||
linkify=True
|
||||
)
|
||||
mark_populated = columns.BooleanColumn(
|
||||
verbose_name=_('Marked Populated'),
|
||||
false_mark=None
|
||||
)
|
||||
mark_utilized = columns.BooleanColumn(
|
||||
verbose_name=_('Marked Utilized'),
|
||||
false_mark=None
|
||||
@ -288,7 +293,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||
model = IPRange
|
||||
fields = (
|
||||
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
|
||||
'mark_utilized', 'utilization', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'mark_populated', 'mark_utilized', 'utilization', 'description', 'comments', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||
@ -303,8 +309,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||
#
|
||||
|
||||
class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
address = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_LINK,
|
||||
address = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('IP Address')
|
||||
)
|
||||
vrf = tables.TemplateColumn(
|
||||
@ -369,6 +375,16 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
}
|
||||
|
||||
|
||||
class AnnotatedIPAddressTable(IPAddressTable):
|
||||
address = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_LINK,
|
||||
verbose_name=_('IP Address')
|
||||
)
|
||||
|
||||
class Meta(IPAddressTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class IPAddressAssignTable(NetBoxTable):
|
||||
address = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_ASSIGN_LINK,
|
||||
|
@ -26,12 +26,12 @@ PREFIX_LINK_WITH_DEPTH = """
|
||||
""" + PREFIX_LINK
|
||||
|
||||
IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}" id="ipaddress_{{ record.pk }}">{{ record.address }}</a>
|
||||
{% if record.address or record.start_address %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record }}</a>
|
||||
{% elif perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}&return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-sm btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.first_ip }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}&return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-sm btn-success">{{ record.title }}</a>
|
||||
{% else %}
|
||||
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
|
||||
{{ record.title }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
@ -918,7 +918,9 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
tenant=None,
|
||||
role=None,
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE,
|
||||
description='foobar1'
|
||||
description='foobar1',
|
||||
mark_populated=True,
|
||||
mark_utilized=True,
|
||||
),
|
||||
IPRange(
|
||||
start_address='10.0.2.100/24',
|
||||
@ -955,7 +957,9 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vrf=None,
|
||||
tenant=None,
|
||||
role=None,
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE
|
||||
status=IPRangeStatusChoices.STATUS_ACTIVE,
|
||||
mark_populated=True,
|
||||
mark_utilized=True,
|
||||
),
|
||||
IPRange(
|
||||
start_address='2001:db8:0:2::1/64',
|
||||
@ -1051,6 +1055,18 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||
|
||||
def test_mark_utilized(self):
|
||||
params = {'mark_utilized': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'mark_utilized': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_mark_populated(self):
|
||||
params = {'mark_populated': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'mark_populated': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
|
||||
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = IPAddress.objects.all()
|
||||
|
@ -211,16 +211,25 @@ class TestPrefix(TestCase):
|
||||
IPAddress(address=IPNetwork('10.0.0.5/26')),
|
||||
IPAddress(address=IPNetwork('10.0.0.7/26')),
|
||||
))
|
||||
# Range is not marked as populated, so it doesn't count against available IP space
|
||||
IPRange.objects.create(
|
||||
start_address=IPNetwork('10.0.0.9/26'),
|
||||
end_address=IPNetwork('10.0.0.12/26')
|
||||
end_address=IPNetwork('10.0.0.10/26')
|
||||
)
|
||||
# Populated range reduces available IP space
|
||||
IPRange.objects.create(
|
||||
start_address=IPNetwork('10.0.0.12/26'),
|
||||
end_address=IPNetwork('10.0.0.13/26'),
|
||||
mark_populated=True
|
||||
)
|
||||
missing_ips = IPSet([
|
||||
'10.0.0.2/32',
|
||||
'10.0.0.4/32',
|
||||
'10.0.0.6/32',
|
||||
'10.0.0.8/32',
|
||||
'10.0.0.13/32',
|
||||
'10.0.0.9/32',
|
||||
'10.0.0.10/32',
|
||||
'10.0.0.11/32',
|
||||
'10.0.0.14/32',
|
||||
])
|
||||
available_ips = parent_prefix.get_available_ips()
|
||||
@ -286,8 +295,12 @@ class TestPrefix(TestCase):
|
||||
])
|
||||
self.assertEqual(prefix.get_utilization(), 32 / 254 * 100) # ~12.5% utilization
|
||||
|
||||
# Create a child range with 32 additional IPs
|
||||
IPRange.objects.create(start_address=IPNetwork('10.0.0.33/24'), end_address=IPNetwork('10.0.0.64/24'))
|
||||
# Create a utilized child range with 32 additional IPs
|
||||
IPRange.objects.create(
|
||||
start_address=IPNetwork('10.0.0.33/24'),
|
||||
end_address=IPNetwork('10.0.0.64/24'),
|
||||
mark_utilized=True
|
||||
)
|
||||
self.assertEqual(prefix.get_utilization(), 64 / 254 * 100) # ~25% utilization
|
||||
|
||||
#
|
||||
@ -569,6 +582,27 @@ class TestIPAddress(TestCase):
|
||||
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
|
||||
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
|
||||
|
||||
#
|
||||
# Range validation
|
||||
#
|
||||
|
||||
def test_create_ip_in_unpopulated_range(self):
|
||||
IPRange.objects.create(
|
||||
start_address=IPNetwork('192.0.2.1/24'),
|
||||
end_address=IPNetwork('192.0.2.100/24')
|
||||
)
|
||||
ip = IPAddress(address=IPNetwork('192.0.2.10/24'))
|
||||
ip.full_clean()
|
||||
|
||||
def test_create_ip_in_populated_range(self):
|
||||
IPRange.objects.create(
|
||||
start_address=IPNetwork('192.0.2.1/24'),
|
||||
end_address=IPNetwork('192.0.2.100/24'),
|
||||
mark_populated=True
|
||||
)
|
||||
ip = IPAddress(address=IPNetwork('192.0.2.10/24'))
|
||||
self.assertRaises(ValidationError, ip.full_clean)
|
||||
|
||||
|
||||
class TestVLANGroup(TestCase):
|
||||
|
||||
|
@ -1,17 +1,38 @@
|
||||
from dataclasses import dataclass
|
||||
import netaddr
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .constants import *
|
||||
from .models import Prefix, VLAN
|
||||
|
||||
__all__ = (
|
||||
'add_available_ipaddresses',
|
||||
'AvailableIPSpace',
|
||||
'add_available_vlans',
|
||||
'add_requested_prefixes',
|
||||
'annotate_ip_space',
|
||||
'get_next_available_prefix',
|
||||
'rebuild_prefixes',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AvailableIPSpace:
|
||||
"""
|
||||
A representation of available IP space between two IP addresses/ranges.
|
||||
"""
|
||||
size: int
|
||||
first_ip: str
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
if self.size == 1:
|
||||
return _('1 IP available')
|
||||
if self.size <= 65536:
|
||||
return _('{count} IPs available').format(count=self.size)
|
||||
return _('Many IPs available')
|
||||
|
||||
|
||||
def add_requested_prefixes(parent, prefix_list, show_available=True, show_assigned=True):
|
||||
"""
|
||||
Return a list of requested prefixes using show_available, show_assigned filters. If available prefixes are
|
||||
@ -42,50 +63,69 @@ def add_requested_prefixes(parent, prefix_list, show_available=True, show_assign
|
||||
return child_prefixes
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
|
||||
considered usable (regardless of mask length).
|
||||
"""
|
||||
def annotate_ip_space(prefix):
|
||||
# Compile child objects
|
||||
records = []
|
||||
records.extend([
|
||||
(iprange.start_address.ip, iprange) for iprange in prefix.get_child_ranges(mark_populated=True)
|
||||
])
|
||||
records.extend([
|
||||
(ip.address.ip, ip) for ip in prefix.get_child_ips()
|
||||
])
|
||||
records = sorted(records, key=lambda x: x[0])
|
||||
|
||||
# Determine the first & last valid IP addresses in the prefix
|
||||
if prefix.family == 4 and prefix.mask_length < 31 and not prefix.is_pool:
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last)
|
||||
|
||||
if not records:
|
||||
return [
|
||||
AvailableIPSpace(
|
||||
size=int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
first_ip=f'{first_ip_in_prefix}/{prefix.mask_length}'
|
||||
)
|
||||
]
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
return [(
|
||||
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
)]
|
||||
|
||||
# Account for any available IPs before the first real IP
|
||||
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
if records[0][0] > first_ip_in_prefix:
|
||||
output.append(AvailableIPSpace(
|
||||
size=int(records[0][0] - first_ip_in_prefix),
|
||||
first_ip=f'{first_ip_in_prefix}/{prefix.mask_length}'
|
||||
))
|
||||
|
||||
# Iterate through existing IPs and annotate free ranges
|
||||
for ip in ipaddress_list:
|
||||
# Add IP ranges & addresses, annotating available space in between records
|
||||
for record in records:
|
||||
if prev_ip:
|
||||
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||
if diff > 1:
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((diff - 1, first_skipped))
|
||||
output.append(ip)
|
||||
prev_ip = ip
|
||||
# Annotate available space
|
||||
if (diff := int(record[0]) - int(prev_ip)) > 1:
|
||||
first_skipped = f'{prev_ip + 1}/{prefix.mask_length}'
|
||||
output.append(AvailableIPSpace(
|
||||
size=diff - 1,
|
||||
first_ip=first_skipped
|
||||
))
|
||||
|
||||
output.append(record[1])
|
||||
|
||||
# Update the previous IP address
|
||||
if hasattr(record[1], 'end_address'):
|
||||
prev_ip = record[1].end_address.ip
|
||||
else:
|
||||
prev_ip = record[0]
|
||||
|
||||
# Include any remaining available IPs
|
||||
if prev_ip.address.ip < last_ip_in_prefix:
|
||||
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
if prev_ip < last_ip_in_prefix:
|
||||
output.append(AvailableIPSpace(
|
||||
size=int(last_ip_in_prefix - prev_ip),
|
||||
first_ip=f'{prev_ip + 1}/{prefix.mask_length}'
|
||||
))
|
||||
|
||||
return output
|
||||
|
||||
|
@ -21,7 +21,7 @@ from . import filtersets, forms, tables
|
||||
from .choices import PrefixStatusChoices
|
||||
from .constants import *
|
||||
from .models import *
|
||||
from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
|
||||
from .utils import add_requested_prefixes, add_available_vlans, annotate_ip_space
|
||||
|
||||
|
||||
#
|
||||
@ -619,7 +619,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
|
||||
class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
queryset = Prefix.objects.all()
|
||||
child_model = IPAddress
|
||||
table = tables.IPAddressTable
|
||||
table = tables.AnnotatedIPAddressTable
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
template_name = 'ipam/prefix/ip_addresses.html'
|
||||
@ -635,7 +635,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
|
||||
def prep_table_data(self, request, queryset, parent):
|
||||
if not request.GET.get('q') and not get_table_ordering(request, self.table):
|
||||
return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool)
|
||||
return annotate_ip_space(parent)
|
||||
return queryset
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
@ -25,12 +25,19 @@
|
||||
<th scope="row">{% trans "Size" %}</th>
|
||||
<td>{{ object.size }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Marked Populated" %}</th>
|
||||
<td>{% checkmark object.mark_populated %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Marked Utilized" %}</th>
|
||||
<td>{% checkmark object.mark_utilized %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Utilization" %}</th>
|
||||
<td>
|
||||
{% if object.mark_utilized %}
|
||||
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
|
||||
<small>({% trans "Marked fully utilized" %})</small>
|
||||
{% else %}
|
||||
{% utilization_graph object.utilization %}
|
||||
{% endif %}
|
||||
|
Loading…
Reference in New Issue
Block a user