diff --git a/awx/main/tasks/host_indirect.py b/awx/main/tasks/host_indirect.py index f00cf85faf70..63d6453650bd 100644 --- a/awx/main/tasks/host_indirect.py +++ b/awx/main/tasks/host_indirect.py @@ -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.') -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 + 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 + # 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 canonical_facts = data['canonical_facts'] @@ -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 """ diff --git a/awx/main/tests/data/projects/host_query/extensions/audit/event_query.yml b/awx/main/tests/data/projects/host_query/extensions/audit/event_query.yml index 1073d98ca332..fb27540cec47 100644 --- a/awx/main/tests/data/projects/host_query/extensions/audit/event_query.yml +++ b/awx/main/tests/data/projects/host_query/extensions/audit/event_query.yml @@ -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}} diff --git a/awx/main/tests/functional/tasks/test_host_indirect.py b/awx/main/tests/functional/tasks/test_host_indirect.py index 8754edffb784..bf9d3b5c325d 100644 --- a/awx/main/tests/functional/tasks/test_host_indirect.py +++ b/awx/main/tests/functional/tasks/test_host_indirect.py @@ -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 @@ -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()): @@ -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 @@ -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, ), diff --git a/awx/main/tests/live/tests/test_indirect_host_counting.py b/awx/main/tests/live/tests/test_indirect_host_counting.py index b4ea8097328b..00eda91db5c9 100644 --- a/awx/main/tests/live/tests/test_indirect_host_counting.py +++ b/awx/main/tests/live/tests/test_indirect_host_counting.py @@ -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) @@ -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