Merge pull request #6392 from netbox-community/4609-prefix-mark-utilized

Closes #4609: Mark prefixes as fully utilized
This commit is contained in:
Jeremy Stretch 2021-05-11 15:12:56 -04:00 committed by GitHub
commit 027b74ad09
11 changed files with 114 additions and 57 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

@ -256,6 +256,21 @@ class RoleTable(BaseTable):
# Prefixes # Prefixes
# #
class PrefixUtilizationColumn(UtilizationColumn):
"""
Extend UtilizationColumn to allow disabling the warning & danger thresholds for prefixes
marked as fully utilized.
"""
template_code = """
{% load helpers %}
{% if record.pk and record.mark_utilized %}
{% utilization_graph value warning_threshold=0 danger_threshold=0 %}
{% elif record.pk %}
{% utilization_graph value %}
{% endif %}
"""
class PrefixTable(BaseTable): class PrefixTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
prefix = tables.TemplateColumn( prefix = tables.TemplateColumn(
@ -283,11 +298,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 = {
@ -296,7 +315,7 @@ class PrefixTable(BaseTable):
class PrefixDetailTable(PrefixTable): class PrefixDetailTable(PrefixTable):
utilization = UtilizationColumn( utilization = PrefixUtilizationColumn(
accessor='get_utilization', accessor='get_utilization',
orderable=False orderable=False
) )
@ -308,7 +327,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,16 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Utilization</th> <th scope="row">Utilization</th>
<td>{% utilization_graph object.get_utilization %}</td> <td>
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
<small>(Marked fully utilized)</small>
{% 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' %}

View File

@ -1,42 +1,18 @@
{% if utilization == 0 %} {% if utilization == 0 %}
<div class="progress align-items-center justify-content-center"> <div class="progress align-items-center justify-content-center">
<span class="w-100 text-center">{{ utilization }}%</span> <span class="w-100 text-center">{{ utilization }}%</span>
</div> </div>
{% else %} {% else %}
<div class="progress"> <div class="progress">
{% if utilization >= danger_threshold %}
<div <div
aria-valuemin="0" role="progressbar"
role="progressbar" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
class="progress-bar bg-danger" aria-valuenow="{{ utilization }}"
aria-valuenow="{{ utilization }}" class="progress-bar {{ bar_class }}"
style="width: {{ utilization }}%;" style="min-width: 8%; width: {{ utilization }}%;"
> >
{{ utilization }}% {{ utilization }}%
</div> </div>
{% elif utilization >= warning_threshold %} </div>
<div
aria-valuemin="0"
role="progressbar"
aria-valuemax="100"
aria-valuenow="{{ utilization }}"
style="width: {{ utilization }}%;"
class="progress-bar bg-warning"
>
{{ utilization }}%
</div>
{% else %}
<div
aria-valuemin="0"
role="progressbar"
aria-valuemax="100"
class="progress-bar bg-success"
aria-valuenow="{{ utilization }}"
style="min-width: 8%;width: {{ utilization }}%;"
>
{{ utilization }}%
</div>
{% endif %}
</div>
{% endif %} {% endif %}

View File

@ -276,10 +276,17 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
""" """
Display a horizontal bar graph indicating a percentage of utilization. Display a horizontal bar graph indicating a percentage of utilization.
""" """
if danger_threshold and utilization >= danger_threshold:
bar_class = 'bg-danger'
elif warning_threshold and utilization >= warning_threshold:
bar_class = 'bg-warning'
elif warning_threshold or danger_threshold:
bar_class = 'bg-success'
else:
bar_class = 'bg-default'
return { return {
'utilization': utilization, 'utilization': utilization,
'warning_threshold': warning_threshold, 'bar_class': bar_class,
'danger_threshold': danger_threshold,
} }