Jelajahi Sumber

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

Closes #4609: Mark prefixes as fully utilized
Jeremy Stretch 4 tahun lalu
induk
melakukan
96ffae3372

+ 3 - 0
docs/release-notes/version-2.12.md

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

+ 1 - 1
netbox/ipam/api/serializers.py

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

+ 1 - 1
netbox/ipam/filtersets.py

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

+ 16 - 4
netbox/ipam/forms.py

@@ -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',
-            'tags',
+            'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
+            '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 - 0
netbox/ipam/migrations/0047_prefix_mark_utilized.py

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

+ 14 - 2
netbox/ipam/models/ip.py

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

+ 22 - 3
netbox/ipam/tables.py

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

+ 8 - 2
netbox/ipam/tests/test_filtersets.py

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

+ 11 - 5
netbox/templates/ipam/prefix.html

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

+ 13 - 37
netbox/templates/utilities/templatetags/utilization_graph.html

@@ -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">
-    {% if utilization >= danger_threshold %}
-    <div 
-        aria-valuemin="0"
-        role="progressbar"
-        aria-valuemax="100"
-        class="progress-bar bg-danger"
-        aria-valuenow="{{ utilization }}"
-        style="width: {{ utilization }}%;"
+  <div class="progress">
+    <div
+      role="progressbar"
+      aria-valuemin="0"
+      aria-valuemax="100"
+      aria-valuenow="{{ utilization }}"
+      class="progress-bar {{ bar_class }}"
+      style="min-width: 8%; width: {{ utilization }}%;"
     >
     >
-        {{ utilization }}%
+      {{ utilization }}%
     </div>
     </div>
-    {% elif utilization >= warning_threshold %}
-    <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 %}
+  </div>
+{% endif %}

+ 9 - 2
netbox/utilities/templatetags/helpers.py

@@ -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,
-        'danger_threshold': danger_threshold,
+        'bar_class': bar_class,
     }
     }