querysets.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. from django.apps import apps
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.contrib.postgres.aggregates import JSONBAgg
  4. from django.db.models import OuterRef, Subquery, Q
  5. from django.db.utils import ProgrammingError
  6. from extras.models.tags import TaggedItem
  7. from utilities.query_functions import EmptyGroupByJSONBAgg
  8. from utilities.querysets import RestrictedQuerySet
  9. class ConfigContextQuerySet(RestrictedQuerySet):
  10. def get_for_object(self, obj, aggregate_data=False):
  11. """
  12. Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
  13. Args:
  14. aggregate_data: If True, use the JSONBAgg aggregate function to return only the list of JSON data objects
  15. """
  16. # `device_role` for Device; `role` for VirtualMachine
  17. role = getattr(obj, 'device_role', None) or obj.role
  18. # Device type and location assignment is relevant only for Devices
  19. device_type = getattr(obj, 'device_type', None)
  20. location = getattr(obj, 'location', None)
  21. # Get assigned cluster, group, and type (if any)
  22. cluster = getattr(obj, 'cluster', None)
  23. cluster_type = getattr(cluster, 'type', None)
  24. cluster_group = getattr(cluster, 'group', None)
  25. # Get the group of the assigned tenant, if any
  26. tenant_group = obj.tenant.group if obj.tenant else None
  27. # Match against the directly assigned region as well as any parent regions.
  28. region = getattr(obj.site, 'region', None)
  29. regions = region.get_ancestors(include_self=True) if region else []
  30. # Match against the directly assigned site group as well as any parent site groups.
  31. sitegroup = getattr(obj.site, 'group', None)
  32. sitegroups = sitegroup.get_ancestors(include_self=True) if sitegroup else []
  33. queryset = self.filter(
  34. Q(regions__in=regions) | Q(regions=None),
  35. Q(site_groups__in=sitegroups) | Q(site_groups=None),
  36. Q(sites=obj.site) | Q(sites=None),
  37. Q(locations=location) | Q(locations=None),
  38. Q(device_types=device_type) | Q(device_types=None),
  39. Q(roles=role) | Q(roles=None),
  40. Q(platforms=obj.platform) | Q(platforms=None),
  41. Q(cluster_types=cluster_type) | Q(cluster_types=None),
  42. Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
  43. Q(clusters=cluster) | Q(clusters=None),
  44. Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
  45. Q(tenants=obj.tenant) | Q(tenants=None),
  46. Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
  47. is_active=True,
  48. ).order_by('weight', 'name').distinct()
  49. if aggregate_data:
  50. return queryset.aggregate(
  51. config_context_data=JSONBAgg('data', ordering=['weight', 'name'])
  52. )['config_context_data']
  53. return queryset
  54. class ConfigContextModelQuerySet(RestrictedQuerySet):
  55. """
  56. QuerySet manager used by models which support ConfigContext (device and virtual machine).
  57. Includes a method which appends an annotation of aggregated config context JSON data objects. This is
  58. implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
  59. This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
  60. multiple objects. This allows the annotation to be entirely optional.
  61. """
  62. def annotate_config_context_data(self):
  63. """
  64. Attach the subquery annotation to the base queryset
  65. """
  66. from extras.models import ConfigContext
  67. return self.annotate(
  68. config_context_data=Subquery(
  69. ConfigContext.objects.filter(
  70. self._get_config_context_filters()
  71. ).annotate(
  72. _data=EmptyGroupByJSONBAgg('data', ordering=['weight', 'name'])
  73. ).values("_data").order_by()
  74. )
  75. ).distinct()
  76. def _get_config_context_filters(self):
  77. # Construct the set of Q objects for the specific object types
  78. tag_query_filters = {
  79. "object_id": OuterRef(OuterRef('pk')),
  80. "content_type__app_label": self.model._meta.app_label,
  81. "content_type__model": self.model._meta.model_name
  82. }
  83. base_query = Q(
  84. Q(platforms=OuterRef('platform')) | Q(platforms=None),
  85. Q(cluster_types=OuterRef('cluster__type')) | Q(cluster_types=None),
  86. Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None),
  87. Q(clusters=OuterRef('cluster')) | Q(clusters=None),
  88. Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
  89. Q(tenants=OuterRef('tenant')) | Q(tenants=None),
  90. Q(
  91. tags__pk__in=Subquery(
  92. TaggedItem.objects.filter(
  93. **tag_query_filters
  94. ).values_list(
  95. 'tag_id',
  96. flat=True
  97. )
  98. )
  99. ) | Q(tags=None),
  100. is_active=True,
  101. )
  102. if self.model._meta.model_name == 'device':
  103. base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND)
  104. base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
  105. base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
  106. base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
  107. region_field = 'site__region'
  108. sitegroup_field = 'site__group'
  109. elif self.model._meta.model_name == 'virtualmachine':
  110. base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
  111. base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
  112. base_query.add(Q(device_types=None), Q.AND)
  113. region_field = 'cluster__site__region'
  114. sitegroup_field = 'cluster__site__group'
  115. base_query.add(
  116. (Q(
  117. regions__tree_id=OuterRef(f'{region_field}__tree_id'),
  118. regions__level__lte=OuterRef(f'{region_field}__level'),
  119. regions__lft__lte=OuterRef(f'{region_field}__lft'),
  120. regions__rght__gte=OuterRef(f'{region_field}__rght'),
  121. ) | Q(regions=None)),
  122. Q.AND
  123. )
  124. base_query.add(
  125. (Q(
  126. site_groups__tree_id=OuterRef(f'{sitegroup_field}__tree_id'),
  127. site_groups__level__lte=OuterRef(f'{sitegroup_field}__level'),
  128. site_groups__lft__lte=OuterRef(f'{sitegroup_field}__lft'),
  129. site_groups__rght__gte=OuterRef(f'{sitegroup_field}__rght'),
  130. ) | Q(site_groups=None)),
  131. Q.AND
  132. )
  133. return base_query
  134. class ObjectChangeQuerySet(RestrictedQuerySet):
  135. def valid_models(self):
  136. # Exclude any change records which refer to an instance of a model that's no longer installed. This
  137. # can happen when a plugin is removed but its data remains in the database, for example.
  138. try:
  139. content_types = ContentType.objects.get_for_models(*apps.get_models()).values()
  140. except ProgrammingError:
  141. # Handle the case where the database schema has not yet been initialized
  142. content_types = ContentType.objects.none()
  143. content_type_ids = set(
  144. ct.pk for ct in content_types
  145. )
  146. return self.filter(changed_object_type_id__in=content_type_ids)