Skip to content

Commit

Permalink
fix: rely on resolved_action instead of task, adapt to proposed query… (
Browse files Browse the repository at this point in the history
#15815)

* fix: rely on resolved_action instead of task, adapt to proposed query structure

* tests: update indirect host tests

* update remaining queries to new format

* update live test
  • Loading branch information
pb82 authored Feb 7, 2025
1 parent dd8c99c commit b2a36c9
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 25 deletions.
34 changes: 22 additions & 12 deletions awx/main/tasks/host_indirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,36 @@ def get_hashable_form(input_data: Union[dict, list, Tuple, int, float, str, bool
raise UnhashableFacts(f'Cannonical facts contains a {type(input_data)} type which can not be hashed.')

Check warning on line 40 in awx/main/tasks/host_indirect.py

View check run for this annotation

Codecov / codecov/patch

awx/main/tasks/host_indirect.py#L40

Added line #L40 was not covered by tests


def build_indirect_host_data(job: Job, job_event_queries: dict[str, str]) -> list[IndirectManagedNodeAudit]:
def build_indirect_host_data(job: Job, job_event_queries: dict[str, dict[str, str]]) -> list[IndirectManagedNodeAudit]:
results = {}
compiled_jq_expressions = {} # Cache for compiled jq expressions
facts_missing_logged = False
unhashable_facts_logged = False
for event in job.job_events.filter(task__in=job_event_queries.keys()).iterator():

for event in job.job_events.filter(event_data__isnull=False).iterator():
if 'res' not in event.event_data:
continue

Check warning on line 51 in awx/main/tasks/host_indirect.py

View check run for this annotation

Codecov / codecov/patch

awx/main/tasks/host_indirect.py#L51

Added line #L51 was not covered by tests

if 'resolved_action' not in event.event_data or event.event_data['resolved_action'] not in job_event_queries.keys():
continue

resolved_action = event.event_data['resolved_action']

# We expect a dict with a 'query' key for the resolved_action
if 'query' not in job_event_queries[resolved_action]:
continue

Check warning on line 60 in awx/main/tasks/host_indirect.py

View check run for this annotation

Codecov / codecov/patch

awx/main/tasks/host_indirect.py#L60

Added line #L60 was not covered by tests

# Recall from cache, or process the jq expression, and loop over the jq results
jq_str_for_event = job_event_queries[event.task]
jq_str_for_event = job_event_queries[resolved_action]['query']

if jq_str_for_event not in compiled_jq_expressions:
compiled_jq_expressions[event.task] = jq.compile(jq_str_for_event)
compiled_jq = compiled_jq_expressions[event.task]
compiled_jq_expressions[resolved_action] = jq.compile(jq_str_for_event)
compiled_jq = compiled_jq_expressions[resolved_action]
for data in compiled_jq.input(event.event_data['res']).all():

# From this jq result (specific to a single Ansible module), get index information about this host record
if not data.get('canonical_facts'):
if not facts_missing_logged:
logger.error(f'jq output missing canonical_facts for module {event.task} on event {event.id} using jq:{jq_str_for_event}')
logger.error(f'jq output missing canonical_facts for module {resolved_action} on event {event.id} using jq:{jq_str_for_event}')
facts_missing_logged = True
continue

Check warning on line 74 in awx/main/tasks/host_indirect.py

View check run for this annotation

Codecov / codecov/patch

awx/main/tasks/host_indirect.py#L72-L74

Added lines #L72 - L74 were not covered by tests
canonical_facts = data['canonical_facts']
Expand All @@ -86,19 +96,19 @@ def build_indirect_host_data(job: Job, job_event_queries: dict[str, str]) -> lis
results[hashable_facts] = audit_record

# Increment rolling count fields
if event.task not in audit_record.events:
audit_record.events.append(event.task)
if resolved_action not in audit_record.events:
audit_record.events.append(resolved_action)
audit_record.count += 1

return list(results.values())


def fetch_job_event_query(job: Job) -> dict[str, str]:
def fetch_job_event_query(job: Job) -> dict[str, dict[str, str]]:
"""Returns the following data structure
{
"demo.query.example": "{canonical_facts: {host_name: .direct_host_name}}"
"demo.query.example": {"query": {canonical_facts: {host_name: .direct_host_name}}}
}
The keys are fully-qualified Ansible module names, and the values are strings which are jq expressions.
The keys are fully-qualified Ansible module names, and the values are dicts containing jq expressions.
This contains all event query expressions that pertain to the given job
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
{
"demo.query.example": "{canonical_facts: {host_name: .direct_host_name}, facts: {device_type: .device_type}}"
}
demo.query.example:
query: >-
{canonical_facts: {host_name: .direct_host_name}, facts: {device_type: .device_type}}
14 changes: 7 additions & 7 deletions awx/main/tests/functional/tasks/test_host_indirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def bare_job(job_factory):


def create_registered_event(job, task_name='demo.query.example'):
return job.job_events.create(task=task_name, event_data={'res': {'direct_host_name': 'foo_host'}})
return job.job_events.create(event_data={'resolved_action': task_name, 'res': {'direct_host_name': 'foo_host'}})


@pytest.fixture
Expand All @@ -41,7 +41,7 @@ def job_with_counted_event(bare_job):

def create_event_query(fqcn='demo.query'):
module_name = f'{fqcn}.example'
return EventQuery.objects.create(fqcn=fqcn, collection_version='1.0.1', event_query=yaml.dump({module_name: TEST_JQ}, default_flow_style=False))
return EventQuery.objects.create(fqcn=fqcn, collection_version='1.0.1', event_query=yaml.dump({module_name: {'query': TEST_JQ}}, default_flow_style=False))


def create_audit_record(name, job, organization, created=now()):
Expand Down Expand Up @@ -79,20 +79,20 @@ def test_build_with_no_results(bare_job):

@pytest.mark.django_db
def test_collect_an_event(job_with_counted_event):
records = build_indirect_host_data(job_with_counted_event, {'demo.query.example': TEST_JQ})
records = build_indirect_host_data(job_with_counted_event, {'demo.query.example': {'query': TEST_JQ}})
assert len(records) == 1


@pytest.mark.django_db
def test_fetch_job_event_query(bare_job, event_query):
assert fetch_job_event_query(bare_job) == {'demo.query.example': TEST_JQ}
assert fetch_job_event_query(bare_job) == {'demo.query.example': {'query': TEST_JQ}}


@pytest.mark.django_db
def test_fetch_multiple_job_event_query(bare_job):
create_event_query(fqcn='demo.query')
create_event_query(fqcn='demo2.query')
assert fetch_job_event_query(bare_job) == {'demo.query.example': TEST_JQ, 'demo2.query.example': TEST_JQ}
assert fetch_job_event_query(bare_job) == {'demo.query.example': {'query': TEST_JQ}, 'demo2.query.example': {'query': TEST_JQ}}


@pytest.mark.django_db
Expand Down Expand Up @@ -156,8 +156,8 @@ def test_multiple_registered_modules_same_collection(bare_job):
collection_version='1.0.1',
event_query=yaml.dump(
{
'demo.query.example': TEST_JQ,
'demo.query.example2': TEST_JQ,
'demo.query.example': {'query': TEST_JQ},
'demo.query.example2': {'query': TEST_JQ},
},
default_flow_style=False,
),
Expand Down
6 changes: 3 additions & 3 deletions awx/main/tests/live/tests/test_indirect_host_counting.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ def test_indirect_host_counting(live_tmp_folder, run_job_from_playbook):
job = Job.objects.filter(name__icontains='test_indirect_host_counting').order_by('-created').first()
wait_for_events(job) # We must wait for events because system tasks iterate on job.job_events.filter(...)

# Data matches to awx/main/tests/data/projects/host_query/meta/event_query.yml
# Data matches to awx/main/tests/data/projects/host_query/extensions/audit/event_query.yml
# this just does things in-line to be a more localized test for the immediate testing
module_jq_str = '{canonical_facts: {host_name: .direct_host_name}, facts: {device_type: .device_type}}'
event_query = {'demo.query.example': module_jq_str}
event_query = {'demo.query.example': {'query': module_jq_str}}

# Run the task logic directly with local data
results = build_indirect_host_data(job, event_query)
Expand All @@ -34,7 +34,7 @@ def test_indirect_host_counting(live_tmp_folder, run_job_from_playbook):
assert 'host_query' in job.installed_collections['demo.query']
hq_text = job.installed_collections['demo.query']['host_query']
hq_data = yaml.safe_load(hq_text)
assert hq_data == {'demo.query.example': module_jq_str}
assert hq_data == {'demo.query.example': {'query': module_jq_str}}

assert job.ansible_version

Expand Down

0 comments on commit b2a36c9

Please sign in to comment.