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 6055debf04
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',
)
mask_length = django_filters.NumberFilter(
method='filter_mask_length',
label='Mask length',
field_name='prefix',
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(
queryset=VRF.objects.all(),
@ -262,11 +270,6 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre
except (AddrFormatError, ValueError):
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):
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',
'tenant', 'is_pool', 'expand',
]
mask_length__lte = forms.IntegerField(
widget=forms.HiddenInput()
)
q = forms.CharField(
required=False,
label='Search'
@ -511,10 +514,6 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
expand = forms.BooleanField(
required=False,
label='Expand prefix hierarchy'
)
tag = TagFilterField(model)

View File

@ -3,34 +3,20 @@ from utilities.querysets import 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
to do this using .annotate() on the QuerySet to count the unique parents of each prefix, that approach introduces
performance issues at scale.
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
modifications.
Annotate the number of parent and child prefixes for each Prefix. Raw SQL is needed for these subqueries
because we need to cast NULL VRF values to integers for comparison. (NULL != NULL).
"""
queryset = self
stack = []
for p in queryset:
try:
prev_p = stack[-1]
except IndexError:
prev_p = None
if prev_p is not None:
while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
stack.pop()
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))
return self.extra(
select={
'parents': 'SELECT COUNT(U0."prefix") AS "c" '
'FROM "ipam_prefix" U0 '
'WHERE (U0."prefix" >> "ipam_prefix"."prefix" '
'AND COALESCE(U0."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))',
'children': 'SELECT COUNT(U1."prefix") AS "c" '
'FROM "ipam_prefix" U1 '
'WHERE (U1."prefix" << "ipam_prefix"."prefix" '
'AND COALESCE(U1."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))',
}
)

View File

@ -39,10 +39,10 @@ ROLE_VLAN_COUNT = """
"""
PREFIX_LINK = """
{% if record.has_children %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
{% if record.children %}
<span class="text-nowrap" style="padding-left: {{ record.parents }}0px "><i class="fa fa-caret-right"></i></a>
{% else %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
<span class="text-nowrap" style="padding-left: {{ record.parents }}9px">
{% 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>
</span>
@ -336,7 +336,9 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta):
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')
row_attrs = {
'class': lambda record: 'success' if not record.pk else '',
@ -357,11 +359,11 @@ class PrefixDetailTable(PrefixTable):
class Meta(PrefixTable.Meta):
fields = (
'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
'tags',
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
'description', 'tags',
)
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'
).order_by(
'prefix'
).annotate_depth(
limit=0
)
).annotate_tree()
# Add available prefixes to the table if requested
if request.GET.get('show_available', 'true') == 'true':
@ -320,17 +318,12 @@ class RoleBulkDeleteView(BulkDeleteView):
#
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_form = forms.PrefixFilterForm
table = tables.PrefixDetailTable
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):
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)
).prefetch_related(
'site', 'role'
).annotate_depth()
).annotate_tree()
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefix_table.exclude = ('vrf',)
@ -386,7 +379,7 @@ class PrefixPrefixesView(ObjectView):
# Child prefixes table
child_prefixes = prefix.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
'site', 'vlan', 'role',
).annotate_depth(limit=0)
).annotate_tree()
# Add available prefixes to the table if requested
if child_prefixes and request.GET.get('show_available', 'true') == 'true':

View File

@ -7,33 +7,36 @@
</div>
<div class="panel-body">
<form action="." method="get" class="form">
{% for field in filter_form %}
<div class="form-group">
{% if field.name == "q" %}
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
{% elif field|widget_type == 'checkboxinput' %}
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
{% else %}
{{ field.label_tag }}
{{ field }}
{% endif %}
</div>
{% endfor %}
<div class="text-right noprint">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> Apply
</button>
<a href="." class="btn btn-default">
<span class="fa fa-remove" aria-hidden="true"></span> Clear
</a>
{% for field in filter_form.hidden_fields %}
{{ field }}
{% endfor %}
{% for field in filter_form.visible_fields %}
<div class="form-group">
{% if field.name == "q" %}
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
{% elif field|widget_type == 'checkboxinput' %}
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
{% else %}
{{ field.label_tag }}
{{ field }}
{% endif %}
</div>
{% endfor %}
<div class="text-right noprint">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> Apply
</button>
<a href="." class="btn btn-default">
<span class="fa fa-remove" aria-hidden="true"></span> Clear
</a>
</div>
</form>
</div>
</div>

View File

@ -3,7 +3,18 @@
{% block buttons %}
<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>
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
<div class="dropdown">
<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>
{% endblock %}

View File

@ -199,6 +199,14 @@ def has_perms(user, 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
#