querysets.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. from django.contrib.postgres.aggregates import JSONBAgg
  2. from django.db.models import OuterRef, Q, Subquery
  3. from extras.models.tags import TaggedItem
  4. from utilities.query_functions import EmptyGroupByJSONBAgg
  5. from utilities.querysets import RestrictedQuerySet
  6. __all__ = (
  7. 'ConfigContextModelQuerySet',
  8. 'ConfigContextQuerySet',
  9. 'NotificationQuerySet',
  10. 'SharedObjectQuerySet',
  11. )
  12. class ConfigContextQuerySet(RestrictedQuerySet):
  13. def get_for_object(self, obj, aggregate_data=False):
  14. """
  15. Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
  16. Args:
  17. aggregate_data: If True, use the JSONBAgg aggregate function to return only the list of JSON data objects
  18. """
  19. # Device type and location assignment are relevant only for Devices
  20. device_type = getattr(obj, 'device_type', None)
  21. location = getattr(obj, 'location', None)
  22. locations = location.get_ancestors(include_self=True) if location else []
  23. # Get assigned cluster, group, and type (if any)
  24. cluster = getattr(obj, 'cluster', None)
  25. cluster_type = getattr(cluster, 'type', None)
  26. cluster_group = getattr(cluster, 'group', None)
  27. # Get the group of the assigned tenant, if any
  28. tenant_group = obj.tenant.group if obj.tenant else None
  29. # Match against the directly assigned region as well as any parent regions.
  30. region = getattr(obj.site, 'region', None)
  31. regions = region.get_ancestors(include_self=True) if region else []
  32. # Match against the directly assigned site group as well as any parent site groups.
  33. sitegroup = getattr(obj.site, 'group', None)
  34. sitegroups = sitegroup.get_ancestors(include_self=True) if sitegroup else []
  35. # Match against the directly assigned role as well as any parent roles.
  36. device_roles = obj.role.get_ancestors(include_self=True) if obj.role else []
  37. # Match against the directly assigned platform as well as any parent platforms.
  38. platform = getattr(obj, 'platform', None)
  39. platforms = platform.get_ancestors(include_self=True) if platform else []
  40. queryset = self.filter(
  41. Q(regions__in=regions) | Q(regions=None),
  42. Q(site_groups__in=sitegroups) | Q(site_groups=None),
  43. Q(sites=obj.site) | Q(sites=None),
  44. Q(locations__in=locations) | Q(locations=None),
  45. Q(device_types=device_type) | Q(device_types=None),
  46. Q(roles__in=device_roles) | Q(roles=None),
  47. Q(platforms__in=platforms) | Q(platforms=None),
  48. Q(cluster_types=cluster_type) | Q(cluster_types=None),
  49. Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
  50. Q(clusters=cluster) | Q(clusters=None),
  51. Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
  52. Q(tenants=obj.tenant) | Q(tenants=None),
  53. Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
  54. is_active=True,
  55. ).order_by('weight', 'name').distinct()
  56. if aggregate_data:
  57. return queryset.aggregate(
  58. config_context_data=JSONBAgg('data', ordering=['weight', 'name'])
  59. )['config_context_data']
  60. return queryset
  61. class ConfigContextModelQuerySet(RestrictedQuerySet):
  62. """
  63. QuerySet manager used by models which support ConfigContext (device and virtual machine).
  64. Includes a method which appends an annotation of aggregated config context JSON data objects. This is
  65. implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
  66. This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
  67. multiple objects. This allows the annotation to be entirely optional.
  68. """
  69. def annotate_config_context_data(self):
  70. """
  71. Attach the subquery annotation to the base queryset
  72. """
  73. from extras.models import ConfigContext
  74. return self.annotate(
  75. config_context_data=Subquery(
  76. ConfigContext.objects.filter(
  77. self._get_config_context_filters()
  78. ).annotate(
  79. _data=EmptyGroupByJSONBAgg('data', order_by=['weight', 'name'])
  80. ).values("_data").order_by()
  81. )
  82. )
  83. def _get_config_context_filters(self):
  84. # Construct the set of Q objects for the specific object types
  85. tag_query_filters = {
  86. "object_id": OuterRef(OuterRef('pk')),
  87. "content_type__app_label": self.model._meta.app_label,
  88. "content_type__model": self.model._meta.model_name
  89. }
  90. base_query = Q(
  91. Q(cluster_types=OuterRef('cluster__type')) | Q(cluster_types=None),
  92. Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None),
  93. Q(clusters=OuterRef('cluster')) | Q(clusters=None),
  94. Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
  95. Q(tenants=OuterRef('tenant')) | Q(tenants=None),
  96. Q(sites=OuterRef('site')) | Q(sites=None),
  97. Q(
  98. tags__pk__in=Subquery(
  99. TaggedItem.objects.filter(
  100. **tag_query_filters
  101. ).values_list(
  102. 'tag_id',
  103. flat=True
  104. ).distinct()
  105. )
  106. ) | Q(tags=None),
  107. is_active=True,
  108. )
  109. # Apply Location & DeviceType filters only for VirtualMachines
  110. if self.model._meta.model_name == 'device':
  111. base_query.add(
  112. (Q(
  113. locations__tree_id=OuterRef('location__tree_id'),
  114. locations__level__lte=OuterRef('location__level'),
  115. locations__lft__lte=OuterRef('location__lft'),
  116. locations__rght__gte=OuterRef('location__rght'),
  117. ) | Q(locations=None)),
  118. Q.AND
  119. )
  120. base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
  121. elif self.model._meta.model_name == 'virtualmachine':
  122. base_query.add(Q(locations=None), Q.AND)
  123. base_query.add(Q(device_types=None), Q.AND)
  124. # MPTT-based filters
  125. base_query.add(
  126. (Q(
  127. regions__tree_id=OuterRef('site__region__tree_id'),
  128. regions__level__lte=OuterRef('site__region__level'),
  129. regions__lft__lte=OuterRef('site__region__lft'),
  130. regions__rght__gte=OuterRef('site__region__rght'),
  131. ) | Q(regions=None)),
  132. Q.AND
  133. )
  134. base_query.add(
  135. (Q(
  136. site_groups__tree_id=OuterRef('site__group__tree_id'),
  137. site_groups__level__lte=OuterRef('site__group__level'),
  138. site_groups__lft__lte=OuterRef('site__group__lft'),
  139. site_groups__rght__gte=OuterRef('site__group__rght'),
  140. ) | Q(site_groups=None)),
  141. Q.AND
  142. )
  143. base_query.add(
  144. (Q(
  145. roles__tree_id=OuterRef('role__tree_id'),
  146. roles__level__lte=OuterRef('role__level'),
  147. roles__lft__lte=OuterRef('role__lft'),
  148. roles__rght__gte=OuterRef('role__rght'),
  149. ) | Q(roles=None)),
  150. Q.AND
  151. )
  152. base_query.add(
  153. (Q(
  154. platforms__tree_id=OuterRef('platform__tree_id'),
  155. platforms__level__lte=OuterRef('platform__level'),
  156. platforms__lft__lte=OuterRef('platform__lft'),
  157. platforms__rght__gte=OuterRef('platform__rght'),
  158. ) | Q(platforms=None)),
  159. Q.AND
  160. )
  161. return base_query
  162. class NotificationQuerySet(RestrictedQuerySet):
  163. def unread(self):
  164. """
  165. Return only unread notifications.
  166. """
  167. return self.filter(read__isnull=True)
  168. class SharedObjectQuerySet(RestrictedQuerySet):
  169. def restrict_to_shared(self, user):
  170. """
  171. Restrict the queryset to objects which are shared or owned by the given user. Superusers are exempt;
  172. anonymous users see only shared objects. This enforces consistent visibility across the UI, REST API,
  173. and GraphQL API.
  174. """
  175. if user.is_superuser:
  176. return self
  177. if user.is_anonymous:
  178. return self.filter(shared=True)
  179. return self.filter(
  180. Q(shared=True) | Q(user=user)
  181. )