mirror of
https://github.com/ansible/awx.git
synced 2026-10-29 22:03:20 -02:30
fix: do not call .filter() on list before converting to queryset (#16752)
* fix: do not call .filter() on list before converting to queryset * fix: optimize slicing of queryset * fix: preserve existing filters and ordering on the queryset
This commit is contained in:
@@ -282,21 +282,17 @@ class Inventory(CommonModelNameNotUnique, RelatedJobsMixin, OpaQueryPathMixin):
|
||||
|
||||
def get_sliced_hosts(self, host_queryset, slice_number, slice_count):
|
||||
"""
|
||||
Returns a slice of Hosts given a slice number and total slice count, or
|
||||
the original queryset if slicing is not requested.
|
||||
|
||||
NOTE: If slicing is performed, this will return a List[Host] with the
|
||||
resulting slice. If slicing is not performed it will return the
|
||||
original queryset (not evaluating it or forcing it to a list). This
|
||||
puts the burden on the caller to check the resulting type. This is
|
||||
non-ideal because it's easy to get wrong, but I think the only way
|
||||
around it is to force the queryset which has memory implications for
|
||||
large inventories.
|
||||
Returns a queryset of Hosts for a given slice number and total slice
|
||||
count, or the original queryset if slicing is not requested.
|
||||
"""
|
||||
|
||||
if slice_count > 1 and slice_number > 0:
|
||||
offset = slice_number - 1
|
||||
host_queryset = host_queryset[offset::slice_count]
|
||||
# Fetch only PKs to avoid loading the full host records into memory
|
||||
# before slicing the query set. Unfortunately it seems that there is
|
||||
# no good way to slice a queryset without converting it to a list first
|
||||
all_pks = list(host_queryset.values_list('pk', flat=True))
|
||||
sliced_pks = all_pks[offset::slice_count]
|
||||
host_queryset = host_queryset.filter(pk__in=sliced_pks)
|
||||
return host_queryset
|
||||
|
||||
def get_script_data(self, hostvars=False, towervars=False, show_all=False, slice_number=1, slice_count=1):
|
||||
|
||||
@@ -221,3 +221,54 @@ class TestConstructedInventoryFactCache:
|
||||
|
||||
assert source_host.ansible_facts == {'role': 'web', 'deployed': True}
|
||||
assert not constructed_host.ansible_facts, f'Facts were stored on the constructed host: {constructed_host.ansible_facts!r}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSlicedJobFactCache:
|
||||
"""finish_fact_cache must work when get_hosts_for_fact_cache returns a sliced result."""
|
||||
|
||||
def test_sliced_hosts_persist_facts(self, organization, artifacts_dir):
|
||||
"""Sliced jobs must persist facts via get_sliced_hosts queryset.
|
||||
|
||||
Regression test for AAP-97902: get_sliced_hosts() used to return a list
|
||||
when slice_count > 1, causing finish_fact_cache() to crash with
|
||||
AttributeError when calling .filter(). get_sliced_hosts() now converts
|
||||
the step-sliced list back to a QuerySet.
|
||||
"""
|
||||
inv = Inventory.objects.create(organization=organization, name='slice-inv')
|
||||
hosts = [inv.hosts.create(name=f'host{i}') for i in range(4)]
|
||||
|
||||
for h in hosts:
|
||||
h.ansible_facts = {'old': True}
|
||||
h.ansible_facts_modified = now()
|
||||
h.save(update_fields=['ansible_facts', 'ansible_facts_modified'])
|
||||
|
||||
host_qs = inv.hosts.all()
|
||||
sliced_qs = inv.get_sliced_hosts(host_qs, slice_number=1, slice_count=2)
|
||||
|
||||
# get_sliced_hosts must return a QuerySet, not a list
|
||||
assert hasattr(sliced_qs, 'filter'), 'get_sliced_hosts must return a QuerySet'
|
||||
|
||||
sliced_hosts = list(sliced_qs)
|
||||
start_fact_cache(sliced_qs, artifacts_dir=artifacts_dir, timeout=0, inventory_id=inv.id)
|
||||
|
||||
# Simulate ansible writing new facts for each host in the slice
|
||||
for h in sliced_hosts:
|
||||
fact_file = os.path.join(artifacts_dir, 'fact_cache', h.name)
|
||||
future = time.time() + 60
|
||||
with open(fact_file, 'w') as f:
|
||||
json.dump({'updated': True, 'host': h.name}, f)
|
||||
os.utime(fact_file, (future, future))
|
||||
|
||||
# This would crash with AttributeError before the fix
|
||||
finish_fact_cache(sliced_qs, artifacts_dir=artifacts_dir, inventory_id=inv.id)
|
||||
|
||||
for h in hosts:
|
||||
h.refresh_from_db()
|
||||
|
||||
sliced_names = {h.name for h in sliced_hosts}
|
||||
for h in hosts:
|
||||
if h.name in sliced_names:
|
||||
assert h.ansible_facts.get('updated') is True, f'{h.name} should have updated facts'
|
||||
else:
|
||||
assert h.ansible_facts == {'old': True}, f'{h.name} should be unchanged'
|
||||
|
||||
Reference in New Issue
Block a user