Merge pull request #4959 from netbox-community/4639-prefix-annotate-depth

Closes #4639: Replace annotate_depth() on Prefix manager
This commit is contained in:
Jeremy Stretch 2020-08-06 09:42:12 -04:00 committed by GitHub
commit c1e58291b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 91 additions and 86 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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