mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Closes #4609: Allow marking prefixes as fully utilized
This commit is contained in:
parent
f6823b5704
commit
bf56145a09
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized
|
||||||
* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit
|
* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit
|
||||||
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
||||||
|
|
||||||
@ -27,6 +28,8 @@
|
|||||||
* `latitude` and `longitude` are now decimal fields rather than strings
|
* `latitude` and `longitude` are now decimal fields rather than strings
|
||||||
* extras.ContentType
|
* extras.ContentType
|
||||||
* Removed the `display_name` attribute (use `display` instead)
|
* Removed the `display_name` attribute (use `display` instead)
|
||||||
|
* ipam.Prefix
|
||||||
|
* Added the `mark_utilized` boolean field
|
||||||
* ipam.VLAN
|
* ipam.VLAN
|
||||||
* Removed the `display_name` attribute (use `display` instead)
|
* Removed the `display_name` attribute (use `display` instead)
|
||||||
* ipam.VRF
|
* ipam.VRF
|
||||||
|
@ -202,7 +202,7 @@ class PrefixSerializer(PrimaryModelSerializer):
|
|||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
|
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
|
||||||
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
read_only_fields = ['family']
|
read_only_fields = ['family']
|
||||||
|
|
||||||
|
@ -304,7 +304,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['id', 'is_pool']
|
fields = ['id', 'is_pool', 'mark_utilized']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -454,11 +454,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
|
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||||
'tags',
|
'tenant_group', 'tenant', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')),
|
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
|
||||||
('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
|
('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
)
|
)
|
||||||
@ -582,6 +582,11 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
widget=BulkEditNullBooleanSelect(),
|
widget=BulkEditNullBooleanSelect(),
|
||||||
label='Is a pool'
|
label='Is a pool'
|
||||||
)
|
)
|
||||||
|
mark_utilized = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect(),
|
||||||
|
label='Treat as 100% utilized'
|
||||||
|
)
|
||||||
description = forms.CharField(
|
description = forms.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
required=False
|
required=False
|
||||||
@ -597,7 +602,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
model = Prefix
|
model = Prefix
|
||||||
field_order = [
|
field_order = [
|
||||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id',
|
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id',
|
||||||
'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool',
|
'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized',
|
||||||
]
|
]
|
||||||
mask_length__lte = forms.IntegerField(
|
mask_length__lte = forms.IntegerField(
|
||||||
widget=forms.HiddenInput()
|
widget=forms.HiddenInput()
|
||||||
@ -675,6 +680,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
mark_utilized = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
label=_('Marked as 100% utilized'),
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
16
netbox/ipam/migrations/0047_prefix_mark_utilized.py
Normal file
16
netbox/ipam/migrations/0047_prefix_mark_utilized.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0046_set_vlangroup_scope_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='mark_utilized',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -288,6 +288,10 @@ class Prefix(PrimaryModel):
|
|||||||
default=False,
|
default=False,
|
||||||
help_text='All IP addresses within this prefix are considered usable'
|
help_text='All IP addresses within this prefix are considered usable'
|
||||||
)
|
)
|
||||||
|
mark_utilized = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Treat as 100% utilized"
|
||||||
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
@ -296,10 +300,11 @@ class Prefix(PrimaryModel):
|
|||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description',
|
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized',
|
||||||
|
'description',
|
||||||
]
|
]
|
||||||
clone_fields = [
|
clone_fields = [
|
||||||
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -364,6 +369,7 @@ class Prefix(PrimaryModel):
|
|||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
self.role.name if self.role else None,
|
self.role.name if self.role else None,
|
||||||
self.is_pool,
|
self.is_pool,
|
||||||
|
self.mark_utilized,
|
||||||
self.description,
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -422,6 +428,9 @@ class Prefix(PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
Return all available IPs within this prefix as an IPSet.
|
Return all available IPs within this prefix as an IPSet.
|
||||||
"""
|
"""
|
||||||
|
if self.mark_utilized:
|
||||||
|
return list()
|
||||||
|
|
||||||
prefix = netaddr.IPSet(self.prefix)
|
prefix = netaddr.IPSet(self.prefix)
|
||||||
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
|
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
|
||||||
available_ips = prefix - child_ips
|
available_ips = prefix - child_ips
|
||||||
@ -461,6 +470,9 @@ class Prefix(PrimaryModel):
|
|||||||
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
||||||
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
|
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
|
||||||
"""
|
"""
|
||||||
|
if self.mark_utilized:
|
||||||
|
return 100
|
||||||
|
|
||||||
if self.status == PrefixStatusChoices.STATUS_CONTAINER:
|
if self.status == PrefixStatusChoices.STATUS_CONTAINER:
|
||||||
queryset = Prefix.objects.filter(
|
queryset = Prefix.objects.filter(
|
||||||
prefix__net_contained=str(self.prefix),
|
prefix__net_contained=str(self.prefix),
|
||||||
|
@ -283,11 +283,15 @@ class PrefixTable(BaseTable):
|
|||||||
is_pool = BooleanColumn(
|
is_pool = BooleanColumn(
|
||||||
verbose_name='Pool'
|
verbose_name='Pool'
|
||||||
)
|
)
|
||||||
|
mark_utilized = BooleanColumn(
|
||||||
|
verbose_name='Marked Utilized'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
|
'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'mark_utilized',
|
||||||
|
'description',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
@ -308,7 +312,7 @@ class PrefixDetailTable(PrefixTable):
|
|||||||
class Meta(PrefixTable.Meta):
|
class Meta(PrefixTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
|
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
|
||||||
'description', 'tags',
|
'mark_utilized', 'description', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
||||||
|
@ -389,11 +389,11 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
prefixes = (
|
prefixes = (
|
||||||
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True),
|
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
||||||
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
||||||
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||||
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||||
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True),
|
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
||||||
Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
||||||
Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||||
Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||||
@ -417,6 +417,12 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'is_pool': 'false'}
|
params = {'is_pool': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||||
|
|
||||||
|
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(), 8)
|
||||||
|
|
||||||
def test_within(self):
|
def test_within(self):
|
||||||
params = {'within': '10.0.0.0/16'}
|
params = {'within': '10.0.0.0/16'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Prefix
|
Prefix
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
||||||
@ -20,7 +20,6 @@
|
|||||||
<span class="badge bg-info">Not a Pool</span>
|
<span class="badge bg-info">Not a Pool</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Family</th>
|
<th scope="row">Family</th>
|
||||||
@ -101,9 +100,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Utilization</th>
|
<th scope="row">Utilization</th>
|
||||||
<td>{% utilization_graph object.get_utilization %}</td>
|
<td>
|
||||||
|
{% if object.marked_utilized %}
|
||||||
|
{% utilization_graph 100 %}
|
||||||
|
{% else %}
|
||||||
|
{% utilization_graph object.get_utilization %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
Loading…
Reference in New Issue
Block a user