Преглед изворни кода

Closes #4609: Allow marking prefixes as fully utilized

jeremystretch пре 4 година
родитељ
комит
bf56145a09

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

@@ -4,6 +4,7 @@
 
 ### 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
 * [#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
 * extras.ContentType
     * Removed the `display_name` attribute (use `display` instead)
+* ipam.Prefix
+    * Added the `mark_utilized` boolean field
 * ipam.VLAN
     * Removed the `display_name` attribute (use `display` instead)
 * ipam.VRF

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

@@ -202,7 +202,7 @@ class PrefixSerializer(PrimaryModelSerializer):
         model = Prefix
         fields = [
             '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']
 

+ 1 - 1
netbox/ipam/filtersets.py

@@ -304,7 +304,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
 
     class Meta:
         model = Prefix
-        fields = ['id', 'is_pool']
+        fields = ['id', 'is_pool', 'mark_utilized']
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 16 - 4
netbox/ipam/forms.py

@@ -454,11 +454,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     class Meta:
         model = Prefix
         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 = (
-            ('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')),
             ('Tenancy', ('tenant_group', 'tenant')),
         )
@@ -582,6 +582,11 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
         widget=BulkEditNullBooleanSelect(),
         label='Is a pool'
     )
+    mark_utilized = forms.NullBooleanField(
+        required=False,
+        widget=BulkEditNullBooleanSelect(),
+        label='Treat as 100% utilized'
+    )
     description = forms.CharField(
         max_length=100,
         required=False
@@ -597,7 +602,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
     model = Prefix
     field_order = [
         '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(
         widget=forms.HiddenInput()
@@ -675,6 +680,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
             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)
 
 

+ 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,
         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(
         max_length=200,
         blank=True
@@ -296,10 +300,11 @@ class Prefix(PrimaryModel):
     objects = PrefixQuerySet.as_manager()
 
     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 = [
-        'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
+        'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
     ]
 
     class Meta:
@@ -364,6 +369,7 @@ class Prefix(PrimaryModel):
             self.get_status_display(),
             self.role.name if self.role else None,
             self.is_pool,
+            self.mark_utilized,
             self.description,
         )
 
@@ -422,6 +428,9 @@ class Prefix(PrimaryModel):
         """
         Return all available IPs within this prefix as an IPSet.
         """
+        if self.mark_utilized:
+            return list()
+
         prefix = netaddr.IPSet(self.prefix)
         child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_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
         "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:
             queryset = Prefix.objects.filter(
                 prefix__net_contained=str(self.prefix),

+ 6 - 2
netbox/ipam/tables.py

@@ -283,11 +283,15 @@ class PrefixTable(BaseTable):
     is_pool = BooleanColumn(
         verbose_name='Pool'
     )
+    mark_utilized = BooleanColumn(
+        verbose_name='Marked Utilized'
+    )
 
     class Meta(BaseTable.Meta):
         model = Prefix
         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')
         row_attrs = {
@@ -308,7 +312,7 @@ class PrefixDetailTable(PrefixTable):
     class Meta(PrefixTable.Meta):
         fields = (
             'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
-            'description', 'tags',
+            'mark_utilized', 'description', 'tags',
         )
         default_columns = (
             '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)
 
         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.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='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: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),
@@ -417,6 +417,12 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'is_pool': 'false'}
         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):
         params = {'within': '10.0.0.0/16'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)

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

@@ -7,10 +7,10 @@
     <div class="col col-md-5">
         <div class="card">
             <h5 class="card-header">
-                Prefix
+              Prefix
             </h5>
             <div class="card-body">
-            <table class="table table-hover attr-table">
+              <table class="table table-hover attr-table">
                 <tr>
                     <td colspan="2">
                     <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>
                     {% endif %}
                     </td>
-
                 </tr>
                 <tr>
                     <th scope="row">Family</th>
@@ -101,9 +100,15 @@
                 </tr>
                 <tr>
                     <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>
-            </table>
+              </table>
             </div>
         </div>
         {% include 'inc/custom_fields_panel.html' %}