Closes #4609: Allow marking prefixes as fully utilized

This commit is contained in:
jeremystretch 2021-05-11 14:03:50 -04:00
parent f6823b5704
commit bf56145a09
9 changed files with 75 additions and 17 deletions

View File

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

View File

@ -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']

View File

@ -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():

View File

@ -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)

View 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),
),
]

View File

@ -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),

View File

@ -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',

View File

@ -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)

View File

@ -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' %}