Add custom last_job_host_summary__failed filter to all host list endpoints (#16699)

* Add custom last_job_host_summary__failed filter to HostList

The denormalized last_job_host_summary FK was removed (AAP-68024), so
filtering by ?last_job_host_summary__failed=True on /v2/hosts/ would
400 or return stale results. Add custom handling that annotates the
queryset with a Subquery on JobHostSummary (same pattern used by the
dashboard counts) and reserves the param names from FieldLookupBackend
so the generic filter skips them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Extend last_job_host_summary failed filter to inventory and group host lists

Build on AlanCoding's HostList filter (ansible/awx#16698) by moving the
Subquery-based latest-summary logic into HostRelatedSearchMixin so
InventoryHostsList, GroupHostsList, and GroupAllHostsList honor the same
query params as the global hosts list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address review: validate filter values and apply both summary filters

Require True for last_job_host_summary failed query params, apply failed
and not-failed filters independently (AND semantics), add GroupHostsList
coverage, and document mixin helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lgalis
2026-10-14 17:56:14 -04:00
committed by GitHub
co-authored by Cursor
parent e5227b4601
commit 1ac181813a
2 changed files with 160 additions and 6 deletions
+40 -5
View File
@@ -35,7 +35,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
# Django REST Framework
from rest_framework.exceptions import APIException, PermissionDenied, ParseError, NotFound
from rest_framework.exceptions import APIException, PermissionDenied, ParseError, NotFound, ValidationError
from rest_framework.parsers import FormParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer
@@ -1872,6 +1872,8 @@ class CredentialTypeExternalTest(OIDCCredentialTestMixin, SubDetailAPIView):
class HostRelatedSearchMixin(object):
rest_filters_reserved_names = ('last_job_host_summary__failed', 'not__last_job_host_summary__failed')
@property
def related_search_fields(self):
# Edge-case handle: https://github.com/ansible/ansible-tower/issues/7712
@@ -1879,6 +1881,39 @@ class HostRelatedSearchMixin(object):
ret.append('ansible_facts')
return ret
def _validate_host_latest_summary_filter_value(self, param_name, value):
"""Latest-summary host filters only support the documented ``?param=True`` form."""
if value != 'True':
raise ValidationError({param_name: _('filter value must be True')})
def _apply_latest_summary_failed_filter(self, qs):
"""Filter hosts by whether their latest JobHostSummary failed.
Honors ``last_job_host_summary__failed`` and ``not__last_job_host_summary__failed``
with the same AND semantics as other list query string filters.
"""
params = self.request.query_params
failed_param = params.get('last_job_host_summary__failed', None)
not_failed_param = params.get('not__last_job_host_summary__failed', None)
if failed_param is None and not_failed_param is None:
return qs
if failed_param is not None:
self._validate_host_latest_summary_filter_value('last_job_host_summary__failed', failed_param)
if not_failed_param is not None:
self._validate_host_latest_summary_filter_value('not__last_job_host_summary__failed', not_failed_param)
latest_failed = Subquery(models.JobHostSummary.objects.filter(host_id=OuterRef('pk')).order_by('-id').values('failed')[:1])
qs = qs.annotate(_latest_failed=latest_failed)
if failed_param is not None:
qs = qs.filter(_latest_failed=True)
if not_failed_param is not None:
qs = qs.filter(_latest_failed=False)
return qs
def _with_host_list_queryset(self, qs):
"""Apply latest-summary list filters and serializer annotations."""
qs = self._apply_latest_summary_failed_filter(qs)
return qs.with_latest_summary_id()
class HostMetricList(ListAPIView):
name = _("Host Metrics List")
@@ -1935,7 +1970,7 @@ class HostList(HostRelatedSearchMixin, ListCreateAPIView):
filter_qs = SmartFilter.query_from_string(filter_string)
qs &= filter_qs
qs = qs.distinct()
return qs.with_latest_summary_id()
return self._with_host_list_queryset(qs)
def list(self, *args, **kwargs):
try:
@@ -1987,7 +2022,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie
resource_purpose = 'hosts of an inventory'
def get_queryset(self):
return super().get_queryset().with_latest_summary_id()
return self._with_host_list_queryset(super().get_queryset())
class HostGroupsList(SubListCreateAttachDetachAPIView):
@@ -2173,7 +2208,7 @@ class GroupHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView):
resource_purpose = 'hosts of a group'
def get_queryset(self):
return super().get_queryset().with_latest_summary_id()
return self._with_host_list_queryset(super().get_queryset())
def update_raw_data(self, data):
data.pop('inventory', None)
@@ -2206,7 +2241,7 @@ class GroupAllHostsList(HostRelatedSearchMixin, SubListAPIView):
self.check_parent_access(parent)
qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator
sublist_qs = parent.all_hosts.distinct()
return (qs & sublist_qs).with_latest_summary_id()
return self._with_host_list_queryset(qs & sublist_qs)
class GroupInventorySourcesList(SubListAPIView):