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

add support for regions and vms

John Anderson 5 лет назад
Родитель
Сommit
22d2289ed2

+ 16 - 12
netbox/dcim/api/views.py

@@ -340,20 +340,24 @@ class DeviceViewSet(CustomFieldModelViewSet):
     queryset = Device.objects.prefetch_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
         'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
-    ).add_config_context_annotation()
-
-    #queryset = Device.objects.annotate(
-    #    config_contexts=Subquery(
-    #        ConfigContext.objects.filter(
-    #            Q(sites=OuterRef('site')) | Q(sites=None)
-    #        ).annotate(
-    #            _data=EmptyGroupByJSONBAgg('data')
-    #        ).values("_data")
-    #    )
-    #)
-
+    )
     filterset_class = filters.DeviceFilterSet
 
+    def get_queryset(self):
+        """
+        Build the proper queryset based on the request context
+
+        If the `brief` query param equates to True or the `exclude` query param
+        includes `config_context` as a value, return the base queryset.
+
+        Else, return the queryset annotated with config context data
+        """
+
+        request = self.get_serializer_context()['request']
+        if request.query_params.get('brief') or 'config_context' in request.query_params.get('exclude', []):
+            return self.queryset
+        return self.queryset.annotate_config_context_data()
+
     def get_serializer_class(self):
         """
         Select the specific serializer based on the request context.

+ 2 - 2
netbox/dcim/models/devices.py

@@ -15,7 +15,7 @@ from taggit.managers import TaggableManager
 from dcim.choices import *
 from dcim.constants import *
 from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
-from extras.querysets import ConfigContextQuerySetMixin
+from extras.querysets import ConfigContextModelQuerySet
 from extras.utils import extras_features
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
@@ -595,7 +595,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    objects = ConfigContextQuerySetMixin.as_manager()
+    objects = ConfigContextModelQuerySet.as_manager()
 
     csv_headers = [
         'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',

+ 1 - 1
netbox/dcim/views.py

@@ -1163,7 +1163,7 @@ class DeviceConfigView(ObjectView):
 
 
 class DeviceConfigContextView(ObjectConfigContextView):
-    queryset = Device.objects.all()
+    queryset = Device.objects.annotate_config_context_data()
     base_template = 'dcim/device.html'
 
 

+ 1 - 3
netbox/extras/models/models.py

@@ -542,10 +542,8 @@ class ConfigContextModel(models.Model):
 
         # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
         data = OrderedDict()
-        #for context in ConfigContext.objects.get_for_object(self):
-        #    data = deepmerge(data, context.data)
 
-        for context in self.config_contexts:
+        for context in self.config_context_data:
             data = deepmerge(data, context)
 
         # If the object has local config context data defined, merge it last

+ 39 - 29
netbox/extras/querysets.py

@@ -1,8 +1,8 @@
 from collections import OrderedDict
 
-from django.contrib.postgres.aggregates import JSONBAgg
 from django.db.models import OuterRef, Subquery, Q, QuerySet
 
+from utilities.query_functions import EmptyGroupByJSONBAgg
 from utilities.querysets import RestrictedQuerySet
 
 
@@ -60,18 +60,14 @@ class ConfigContextQuerySet(RestrictedQuerySet):
         ).order_by('weight', 'name')
 
 
-class EmptyGroupByJSONBAgg(JSONBAgg):
-    contains_aggregate = False
+class ConfigContextModelQuerySet(RestrictedQuerySet):
 
-
-class ConfigContextQuerySetMixin(RestrictedQuerySet):
-
-    def add_config_context_annotation(self):
+    def annotate_config_context_data(self):
         from extras.models import ConfigContext
         return self.annotate(
-            config_contexts=Subquery(
+            config_context_data=Subquery(
                 ConfigContext.objects.filter(
-                    self._add_config_context_filters()
+                    self._get_config_context_filters()
                 ).order_by(
                     'weight',
                     'name'
@@ -81,28 +77,42 @@ class ConfigContextQuerySetMixin(RestrictedQuerySet):
             )
         )
 
-    def _add_config_context_filters(self):
+    def _get_config_context_filters(self):
 
+        base_query = Q(
+            Q(platforms=OuterRef('platform')) | Q(platforms=None),
+            Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
+            Q(tenants=OuterRef('tenant')) | Q(tenants=None),
+            Q(tags=OuterRef('tags')) | Q(tags=None),
+            is_active=True,
+        )
 
         if self.model._meta.model_name == 'device':
-            return Q(
-                Q(sites=OuterRef('site')) | Q(sites=None),
-                Q(roles=OuterRef('device_role')) | Q(roles=None),
-                Q(platforms=OuterRef('platform')) | Q(platforms=None),
-                Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
-                Q(tenants=OuterRef('tenant')) | Q(tenants=None),
-                Q(tags=OuterRef('tags')) | Q(tags=None),
-                is_active=True,
+            base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
+            base_query.add(
+                (Q(
+                    regions__tree_id=OuterRef('site__region__tree_id'),
+                    regions__level__lte=OuterRef('site__region__level'),
+                    regions__lft__lte=OuterRef('site__region__lft'),
+                    regions__rght__gte=OuterRef('site__region__rght'),
+                ) | Q(regions=None)),
+                Q.AND
             )
-        else:
-            return Q(
-                Q(sites=OuterRef('site')) | Q(sites=None),
-                Q(roles=OuterRef('role')) | Q(roles=None),
-                Q(platforms=OuterRef('platform')) | Q(platforms=None),
-                Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None),
-                Q(clusters=OuterRef('cluster')) | Q(clusters=None),
-                Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
-                Q(tenants=OuterRef('tenant')) | Q(tenants=None),
-                Q(tags=OuterRef('tags')) | Q(tags=None),
-                is_active=True,
+            base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
+
+        elif self.model._meta.model_name == 'virtualmachine':
+            base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
+            base_query.add((Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None)), Q.AND)
+            base_query.add((Q(clusters=OuterRef('cluster')) | Q(clusters=None)), Q.AND)
+            base_query.add(
+                (Q(
+                    regions__tree_id=OuterRef('cluster__site__region__tree_id'),
+                    regions__level__lte=OuterRef('cluster__site__region__level'),
+                    regions__lft__lte=OuterRef('cluster__site__region__lft'),
+                    regions__rght__gte=OuterRef('cluster__site__region__rght'),
+                ) | Q(regions=None)),
+                Q.AND
             )
+            base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
+
+        return base_query

+ 10 - 0
netbox/utilities/query_functions.py

@@ -1,3 +1,4 @@
+from django.contrib.postgres.aggregates import JSONBAgg
 from django.db.models import F, Func
 
 
@@ -7,3 +8,12 @@ class CollateAsChar(Func):
     """
     function = 'C'
     template = '(%(expressions)s) COLLATE "%(function)s"'
+
+
+class EmptyGroupByJSONBAgg(JSONBAgg):
+    """
+    JSONBAgg is a builtin aggregation function which means it includes the use of a GROUP BY clause.
+    When used as an annotation for collecting config context data objects, the GROUP BY is
+    incorrect. This subclass overrides the Django ORM aggregation control to remove the GROUP BY.
+    """
+    contains_aggregate = False

+ 15 - 0
netbox/virtualization/api/views.py

@@ -64,6 +64,21 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
     )
     filterset_class = filters.VirtualMachineFilterSet
 
+    def get_queryset(self):
+        """
+        Build the proper queryset based on the request context
+
+        If the `brief` query param equates to True or the `exclude` query param
+        includes `config_context` as a value, return the base queryset.
+
+        Else, return the queryset annotated with config context data
+        """
+
+        request = self.get_serializer_context()['request']
+        if request.query_params.get('brief') or 'config_context' in request.query_params.get('exclude', []):
+            return self.queryset
+        return self.queryset.annotate_config_context_data()
+
     def get_serializer_class(self):
         """
         Select the specific serializer based on the request context.

+ 2 - 1
netbox/virtualization/models.py

@@ -8,6 +8,7 @@ from taggit.managers import TaggableManager
 from dcim.choices import InterfaceModeChoices
 from dcim.models import BaseInterface, Device
 from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
+from extras.querysets import ConfigContextModelQuerySet
 from extras.utils import extras_features
 from utilities.fields import NaturalOrderingField
 from utilities.ordering import naturalize_interface
@@ -282,7 +283,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    objects = RestrictedQuerySet.as_manager()
+    objects = ConfigContextModelQuerySet.as_manager()
 
     csv_headers = [
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',

+ 1 - 1
netbox/virtualization/views.py

@@ -261,7 +261,7 @@ class VirtualMachineView(ObjectView):
 
 
 class VirtualMachineConfigContextView(ObjectConfigContextView):
-    queryset = VirtualMachine.objects.all()
+    queryset = VirtualMachine.objects.annotate_config_context_data()
     base_template = 'virtualization/virtualmachine.html'