From b1bc933e9888852194a63a4817210859d572df26 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 8 Aug 2025 09:29:12 -0500 Subject: [PATCH] Clean up Prefix TODOs --- netbox/ipam/forms/bulk_edit.py | 20 +++++++-- netbox/ipam/forms/bulk_import.py | 57 ++++++++++++++++++++++-- netbox/ipam/models/ip.py | 2 +- netbox/ipam/tables/ip.py | 31 ++++++++----- netbox/ipam/tables/template_code.py | 20 ++++++--- netbox/templates/ipam/iprange.html | 4 ++ netbox/utilities/templatetags/helpers.py | 20 +++++++++ 7 files changed, 128 insertions(+), 26 deletions(-) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index cefd59cbc..e727785e1 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -207,7 +207,11 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm): class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): - # TODO: Alter for parent prefix + parent = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + label=_('Parent Prefix') + ) vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), required=False, @@ -267,7 +271,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): model = Prefix fieldsets = ( FieldSet('tenant', 'status', 'role', 'description'), - FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')), + FieldSet('parent', 'vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')), FieldSet('scope_type', 'scope', name=_('Scope')), FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')), ) @@ -277,7 +281,11 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): class IPRangeBulkEditForm(NetBoxModelBulkEditForm): - # TODO: Alter for prefix + prefix = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + label=_('Prefix') + ) vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -325,7 +333,11 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): class IPAddressBulkEditForm(NetBoxModelBulkEditForm): - # TODO: Alter for prefix + prefix = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + label=_('Prefix') + ) prefix = DynamicModelChoiceField( queryset=Prefix.objects.all(), required=False, diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index a632e00a5..b0f7d2229 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -156,8 +156,18 @@ class RoleImportForm(NetBoxModelImportForm): class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm): - # TODO: Alter for aggregate - # TODO: Alter for parent prefix + aggregate = CSVModelChoiceField( + label=_('Aggregate'), + queryset=Aggregate.objects.all(), + to_field_name='prefix', + required=False + ) + parent = CSVModelChoiceField( + label=_('Prefix'), + queryset=Prefix.objects.all(), + to_field_name='prefix', + required=False + ) vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -245,9 +255,26 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm): queryset = self.fields['vlan'].queryset.filter(query) self.fields['vlan'].queryset = queryset + # Limit Prefix queryset by assigned vrf + vrf = data.get('vrf') + query = Q() + if vrf: + query &= Q(**{ + f"vrf__{self.fields['vrf'].to_field_name}": vrf + }) + + queryset = self.fields['parent'].queryset.filter(query) + self.fields['parent'].queryset = queryset + class IPRangeImportForm(NetBoxModelImportForm): - # TODO: Alter for prefix + prefix = CSVModelChoiceField( + label=_('Prefix'), + queryset=Prefix.objects.all(), + to_field_name='prefix', + required=True, + help_text=_('Assigned prefix') + ) vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -282,9 +309,22 @@ class IPRangeImportForm(NetBoxModelImportForm): 'description', 'comments', 'tags', ) + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + # Limit Prefix queryset by assigned vrf + vrf = data.get('vrf') + query = Q() + if vrf: + query &= Q(**{ + f"vrf__{self.fields['vrf'].to_field_name}": vrf + }) + + queryset = self.fields['prefix'].queryset.filter(query) + self.fields['prefix'].queryset = queryset + class IPAddressImportForm(NetBoxModelImportForm): - # TODO: Alter for prefix prefix = CSVModelChoiceField( label=_('Prefix'), queryset=Prefix.objects.all(), @@ -368,6 +408,15 @@ class IPAddressImportForm(NetBoxModelImportForm): if data: + # Limit Prefix queryset by assigned vrf + vrf = data.get('vrf') + query = Q() + if vrf: + query &= Q(**{f"vrf__{self.fields['vrf'].to_field_name}": vrf}) + + queryset = self.fields['prefix'].queryset.filter(query) + self.fields['prefix'].queryset = queryset + # Limit interface queryset by assigned device if data.get('device'): self.fields['interface'].queryset = Interface.objects.filter( diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index ea8c86967..9f9c8884c 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -372,7 +372,7 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary @property def mask_length(self): return self.prefix.prefixlen if self.prefix else None - + @property def ipv6_full(self): if self.prefix and self.prefix.version == 6: diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 967fa009d..3b1f66c37 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -155,7 +155,10 @@ class PrefixUtilizationColumn(columns.UtilizationColumn): class PrefixTable(TenancyColumnsMixin, NetBoxTable): - # TODO: Alter for parent prefix + parent = tables.Column( + verbose_name=_('Parent'), + linkify=True + ) prefix = columns.TemplateColumn( verbose_name=_('Prefix'), template_code=PREFIX_LINK_WITH_DEPTH, @@ -237,9 +240,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Prefix fields = ( - 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', - 'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', - 'tags', 'created', 'last_updated', + 'pk', 'id', 'prefix', 'status', 'parent', 'parent_flat', 'children', 'vrf', 'utilization', + 'tenant', 'tenant_group', 'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', + 'description', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'scope', 'vlan', 'role', @@ -254,7 +257,10 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): # IP ranges # class IPRangeTable(TenancyColumnsMixin, NetBoxTable): - # TODO: Alter for prefix + prefix = tables.Column( + verbose_name=_('Prefix'), + linkify=True + ) start_address = tables.Column( verbose_name=_('Start address'), linkify=True @@ -294,9 +300,9 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = IPRange fields = ( - 'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', - 'mark_populated', 'mark_utilized', 'utilization', 'description', 'comments', 'tags', 'created', - 'last_updated', + 'pk', 'id', 'start_address', 'end_address', 'prefix', 'size', 'vrf', 'status', 'role', 'tenant', + 'tenant_group', 'mark_populated', 'mark_utilized', 'utilization', 'description', 'comments', 'tags', + 'created', 'last_updated', ) default_columns = ( 'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', @@ -311,7 +317,10 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable): # class IPAddressTable(TenancyColumnsMixin, NetBoxTable): - # TODO: Alter for prefix + prefix = tables.Column( + verbose_name=_('Prefix'), + linkify=True + ) address = tables.TemplateColumn( template_code=IPADDRESS_LINK, verbose_name=_('IP Address') @@ -371,8 +380,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = IPAddress fields = ( - 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', - 'assigned', 'dns_name', 'description', 'comments', 'tags', 'created', 'last_updated', + 'pk', 'id', 'address', 'vrf', 'prefix', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', + 'nat_outside', 'assigned', 'dns_name', 'description', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description', diff --git a/netbox/ipam/tables/template_code.py b/netbox/ipam/tables/template_code.py index 14b73b28d..87c426009 100644 --- a/netbox/ipam/tables/template_code.py +++ b/netbox/ipam/tables/template_code.py @@ -16,12 +16,20 @@ PREFIX_COPY_BUTTON = """ PREFIX_LINK_WITH_DEPTH = """ {% load helpers %} -{% if record.depth %} -
- {% for i in record.depth|as_range %} - - {% endfor %} -
+{% if record.depth_count %} + {% if object %} +
+ {% for i in record.depth_count|parent_depth:object|as_range %} + + {% endfor %} +
+ {% else %} +
+ {% for i in record.depth_count|as_range %} + + {% endfor %} +
+ {% endif %} {% endif %} """ + PREFIX_LINK diff --git a/netbox/templates/ipam/iprange.html b/netbox/templates/ipam/iprange.html index c4026848b..d63e2514c 100644 --- a/netbox/templates/ipam/iprange.html +++ b/netbox/templates/ipam/iprange.html @@ -13,6 +13,10 @@ {% trans "Family" %} IPv{{ object.family }} + + {% trans "Prefix" %} + {{ object.prefix|linkify|placeholder }} + {% trans "Starting Address" %} {{ object.start_address }} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 2f175d2b6..01ae88962 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -157,6 +157,26 @@ def as_range(n): return range(n) +@register.filter() +def parent_depth(n, parent=None): + """ + Return the depth of a node based on the parent's depth + """ + parent_depth = 0 + if parent and hasattr(parent, 'depth_count'): + parent_depth = parent.depth_count + 1 + elif parent and hasattr(parent, 'depth'): + try: + parent_depth = int(parent.depth) + 1 + except TypeError: + pass + try: + depth = int(n) - int(parent_depth) + except TypeError: + return n + return depth + + @register.filter() def meters_to_feet(n): """