mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 13:06:30 -06:00
Merge pull request #4959 from netbox-community/4639-prefix-annotate-depth
Closes #4639: Replace annotate_depth() on Prefix manager
This commit is contained in:
commit
c1e58291b2
@ -144,8 +144,16 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre
|
|||||||
label='Prefixes which contain this prefix or IP',
|
label='Prefixes which contain this prefix or IP',
|
||||||
)
|
)
|
||||||
mask_length = django_filters.NumberFilter(
|
mask_length = django_filters.NumberFilter(
|
||||||
method='filter_mask_length',
|
field_name='prefix',
|
||||||
label='Mask length',
|
lookup_expr='net_mask_length'
|
||||||
|
)
|
||||||
|
mask_length__gte = django_filters.NumberFilter(
|
||||||
|
field_name='prefix',
|
||||||
|
lookup_expr='net_mask_length__gte'
|
||||||
|
)
|
||||||
|
mask_length__lte = django_filters.NumberFilter(
|
||||||
|
field_name='prefix',
|
||||||
|
lookup_expr='net_mask_length__lte'
|
||||||
)
|
)
|
||||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@ -262,11 +270,6 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre
|
|||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def filter_mask_length(self, queryset, name, value):
|
|
||||||
if not value:
|
|
||||||
return queryset
|
|
||||||
return queryset.filter(prefix__net_mask_length=value)
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
|
@ -437,6 +437,9 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
|
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
|
||||||
'tenant', 'is_pool', 'expand',
|
'tenant', 'is_pool', 'expand',
|
||||||
]
|
]
|
||||||
|
mask_length__lte = forms.IntegerField(
|
||||||
|
widget=forms.HiddenInput()
|
||||||
|
)
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
@ -511,10 +514,6 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
expand = forms.BooleanField(
|
|
||||||
required=False,
|
|
||||||
label='Expand prefix hierarchy'
|
|
||||||
)
|
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,34 +3,20 @@ from utilities.querysets import RestrictedQuerySet
|
|||||||
|
|
||||||
class PrefixQuerySet(RestrictedQuerySet):
|
class PrefixQuerySet(RestrictedQuerySet):
|
||||||
|
|
||||||
def annotate_depth(self, limit=None):
|
def annotate_tree(self):
|
||||||
"""
|
"""
|
||||||
Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
|
Annotate the number of parent and child prefixes for each Prefix. Raw SQL is needed for these subqueries
|
||||||
to do this using .annotate() on the QuerySet to count the unique parents of each prefix, that approach introduces
|
because we need to cast NULL VRF values to integers for comparison. (NULL != NULL).
|
||||||
performance issues at scale.
|
|
||||||
|
|
||||||
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
|
|
||||||
modifications.
|
|
||||||
"""
|
"""
|
||||||
queryset = self
|
return self.extra(
|
||||||
stack = []
|
select={
|
||||||
for p in queryset:
|
'parents': 'SELECT COUNT(U0."prefix") AS "c" '
|
||||||
try:
|
'FROM "ipam_prefix" U0 '
|
||||||
prev_p = stack[-1]
|
'WHERE (U0."prefix" >> "ipam_prefix"."prefix" '
|
||||||
except IndexError:
|
'AND COALESCE(U0."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))',
|
||||||
prev_p = None
|
'children': 'SELECT COUNT(U1."prefix") AS "c" '
|
||||||
if prev_p is not None:
|
'FROM "ipam_prefix" U1 '
|
||||||
while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
|
'WHERE (U1."prefix" << "ipam_prefix"."prefix" '
|
||||||
stack.pop()
|
'AND COALESCE(U1."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))',
|
||||||
try:
|
}
|
||||||
prev_p = stack[-1]
|
)
|
||||||
except IndexError:
|
|
||||||
prev_p = None
|
|
||||||
break
|
|
||||||
if prev_p is not None:
|
|
||||||
prev_p.has_children = True
|
|
||||||
stack.append(p)
|
|
||||||
p.depth = len(stack) - 1
|
|
||||||
if limit is None:
|
|
||||||
return queryset
|
|
||||||
return list(filter(lambda p: p.depth <= limit, queryset))
|
|
||||||
|
@ -39,10 +39,10 @@ ROLE_VLAN_COUNT = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX_LINK = """
|
PREFIX_LINK = """
|
||||||
{% if record.has_children %}
|
{% if record.children %}
|
||||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
<span class="text-nowrap" style="padding-left: {{ record.parents }}0px "><i class="fa fa-caret-right"></i></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
|
<span class="text-nowrap" style="padding-left: {{ record.parents }}9px">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||||
</span>
|
</span>
|
||||||
@ -336,7 +336,9 @@ class PrefixTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description')
|
fields = (
|
||||||
|
'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', '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 = {
|
||||||
'class': lambda record: 'success' if not record.pk else '',
|
'class': lambda record: 'success' if not record.pk else '',
|
||||||
@ -357,11 +359,11 @@ class PrefixDetailTable(PrefixTable):
|
|||||||
|
|
||||||
class Meta(PrefixTable.Meta):
|
class Meta(PrefixTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
|
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
|
||||||
'tags',
|
'description', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -221,9 +221,7 @@ class AggregateView(ObjectView):
|
|||||||
'site', 'role'
|
'site', 'role'
|
||||||
).order_by(
|
).order_by(
|
||||||
'prefix'
|
'prefix'
|
||||||
).annotate_depth(
|
).annotate_tree()
|
||||||
limit=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add available prefixes to the table if requested
|
# Add available prefixes to the table if requested
|
||||||
if request.GET.get('show_available', 'true') == 'true':
|
if request.GET.get('show_available', 'true') == 'true':
|
||||||
@ -320,17 +318,12 @@ class RoleBulkDeleteView(BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PrefixListView(ObjectListView):
|
class PrefixListView(ObjectListView):
|
||||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role').annotate_tree()
|
||||||
filterset = filters.PrefixFilterSet
|
filterset = filters.PrefixFilterSet
|
||||||
filterset_form = forms.PrefixFilterForm
|
filterset_form = forms.PrefixFilterForm
|
||||||
table = tables.PrefixDetailTable
|
table = tables.PrefixDetailTable
|
||||||
template_name = 'ipam/prefix_list.html'
|
template_name = 'ipam/prefix_list.html'
|
||||||
|
|
||||||
def alter_queryset(self, request):
|
|
||||||
# Show only top-level prefixes by default (unless searching)
|
|
||||||
limit = None if request.GET.get('expand') or request.GET.get('q') else 0
|
|
||||||
return self.queryset.annotate_depth(limit=limit)
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixView(ObjectView):
|
class PrefixView(ObjectView):
|
||||||
queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
|
queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
|
||||||
@ -353,7 +346,7 @@ class PrefixView(ObjectView):
|
|||||||
prefix__net_contains=str(prefix.prefix)
|
prefix__net_contains=str(prefix.prefix)
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'site', 'role'
|
'site', 'role'
|
||||||
).annotate_depth()
|
).annotate_tree()
|
||||||
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
||||||
parent_prefix_table.exclude = ('vrf',)
|
parent_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
@ -386,7 +379,7 @@ class PrefixPrefixesView(ObjectView):
|
|||||||
# Child prefixes table
|
# Child prefixes table
|
||||||
child_prefixes = prefix.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
child_prefixes = prefix.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
||||||
'site', 'vlan', 'role',
|
'site', 'vlan', 'role',
|
||||||
).annotate_depth(limit=0)
|
).annotate_tree()
|
||||||
|
|
||||||
# Add available prefixes to the table if requested
|
# Add available prefixes to the table if requested
|
||||||
if child_prefixes and request.GET.get('show_available', 'true') == 'true':
|
if child_prefixes and request.GET.get('show_available', 'true') == 'true':
|
||||||
|
@ -7,7 +7,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form action="." method="get" class="form">
|
<form action="." method="get" class="form">
|
||||||
{% for field in filter_form %}
|
{% for field in filter_form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for field in filter_form.visible_fields %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{% if field.name == "q" %}
|
{% if field.name == "q" %}
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
@ -3,7 +3,18 @@
|
|||||||
|
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
|
<div class="dropdown">
|
||||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
|
<button class="btn btn-default dropdown-toggle" type="button" id="max_length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||||
|
Max Length{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="max_length">
|
||||||
|
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
|
||||||
|
<li><a href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=i page=1 %}">
|
||||||
|
{{ i }} {% if request.GET.mask_length__lte == i %}<i class="fa fa-check"></i>{% endif %}
|
||||||
|
</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -199,6 +199,14 @@ def has_perms(user, permissions_list):
|
|||||||
return user.has_perms(permissions_list)
|
return user.has_perms(permissions_list)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def split(string, sep=','):
|
||||||
|
"""
|
||||||
|
Split a string by the given value (default: comma)
|
||||||
|
"""
|
||||||
|
return string.split(sep)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user