Просмотр исходного кода

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

Closes #4639: Replace annotate_depth() on Prefix manager
Jeremy Stretch 5 лет назад
Родитель
Сommit
c1e58291b2

+ 10 - 7
netbox/ipam/filters.py

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

+ 3 - 4
netbox/ipam/forms.py

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

+ 15 - 29
netbox/ipam/querysets.py

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

+ 9 - 7
netbox/ipam/tables.py

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

+ 4 - 11
netbox/ipam/views.py

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

+ 29 - 26
netbox/templates/inc/search_panel.html

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

+ 13 - 2
netbox/templates/ipam/prefix_list.html

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

+ 8 - 0
netbox/utilities/templatetags/helpers.py

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