[AAP-74343] Decouple installed_collections/ansible_version from indirect node counting flag (#16453)

* [AAP-74343] Decouple installed_collections and ansible_version from indirect node counting flag

The indirect_instance_count callback plugin and its artifact processing
were entirely gated behind FEATURE_INDIRECT_NODE_COUNTING_ENABLED. This
caused installed_collections and ansible_version to remain unpopulated
when the flag was off, even though these are baseline analytics fields
unrelated to indirect host counting.

Always run the callback plugin and persist installed_collections and
ansible_version to the database. Only the indirect-counting-specific
parts (EventQuery creation, event_queries_processed flag, and vendor
collections) remain gated behind the feature flag.

* [AAP-74343] Read callbacks_enabled from ansible.cfg so user-configured callbacks are preserved

The check for 'callbacks_enabled' in config_values was dead code because
read_ansible_config was never asked to read that setting. Now that the
callback registration runs unconditionally, fix this by including
'callbacks_enabled' in the variables of interest.

* [AAP-74343] Use comma delimiter for ANSIBLE_CALLBACKS_ENABLED

Ansible's CALLBACKS_ENABLED config is type list and splits on commas.
The colon delimiter would cause combined callback names to be treated
as a single invalid name.

* [AAP-74343] Add tests for ANSIBLE_CALLBACKS_ENABLED configuration

Verify that indirect_instance_count is always set, user-configured
callbacks from ansible.cfg are preserved, and the comma delimiter
is used as ansible-core expects.

* [AAP-74343] Use public API for namespace package path access

Replace library.__path__._path[0] with library.__path__[0] to avoid
relying on a private CPython implementation detail of _NamespacePath.

* [AAP-74343] Skip host query scanning when indirect counting flag is off

The indirect_instance_count callback plugin now checks AWX_COLLECT_HOST_QUERIES
to decide whether to scan for host query files. When the feature flag is off,
the plugin only collects collection metadata (name + version) and ansible_version,
skipping the expensive embedded/external query file discovery.

* [AAP-74343] Set AWX_COLLECT_HOST_QUERIES in query discovery tests

The TestExternalQueryDiscovery tests exercise the host query scanning
path, which now requires AWX_COLLECT_HOST_QUERIES=1 in the environment.

* [AAP-74343] Use Ansible plugin config system for collect_host_queries

Declare collect_host_queries as a formal plugin option in DOCUMENTATION
with env var AWX_COLLECT_HOST_QUERIES, replacing the raw os.getenv() call
with self.get_option(). This follows the standard Ansible plugin
configuration pattern.

* [AAP-74343] Add test for disabled collect_host_queries path

Verify that when collect_host_queries is false, the plugin still
enumerates collections for metadata but skips host query file scanning.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dirk Julich
2026-05-22 14:45:08 +02:00
committed by GitHub
parent ec85902b37
commit b37f3892b6
6 changed files with 267 additions and 53 deletions

View File

@@ -1,4 +1,9 @@
from awx.main.tasks.callback import RunnerCallback
import json
import os
import tempfile
from unittest import mock
from awx.main.tasks.callback import RunnerCallback, try_load_query_file
from awx.main.constants import ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE
from django.utils.translation import gettext_lazy as _
@@ -50,3 +55,102 @@ def test_special_ansible_runner_message(mock_me):
'Traceback:\ngot an unexpected keyword argument\nFile: bar.py\n'
f'{ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE}'
)
SAMPLE_ANSIBLE_DATA = {
'installed_collections': {
'ansible.builtin': {'version': '2.16.0'},
'community.general': {'version': '8.0.0', 'host_query': 'SELECT * FROM hosts'},
},
'ansible_version': '2.16.0',
}
class TestTryLoadQueryFile:
def test_loads_file_without_feature_flag(self):
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'ansible_data.json')
with open(path, 'w') as f:
json.dump(SAMPLE_ANSIBLE_DATA, f)
with mock.patch('awx.main.tasks.callback.flag_enabled', return_value=False):
success, data = try_load_query_file(tmpdir)
assert success is True
assert data['ansible_version'] == '2.16.0'
assert 'ansible.builtin' in data['installed_collections']
def test_loads_file_with_feature_flag(self):
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'ansible_data.json')
with open(path, 'w') as f:
json.dump(SAMPLE_ANSIBLE_DATA, f)
with mock.patch('awx.main.tasks.callback.flag_enabled', return_value=True):
success, data = try_load_query_file(tmpdir)
assert success is True
assert data == SAMPLE_ANSIBLE_DATA
def test_returns_false_when_file_missing(self):
with tempfile.TemporaryDirectory() as tmpdir:
success, data = try_load_query_file(tmpdir)
assert success is False
assert data is None
class TestArtifactsHandler:
def test_always_persists_metadata_when_flag_off(self, mock_me):
rc = RunnerCallback()
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'ansible_data.json')
with open(path, 'w') as f:
json.dump(SAMPLE_ANSIBLE_DATA, f)
with mock.patch('awx.main.tasks.callback.flag_enabled', return_value=False):
rc.artifacts_handler(tmpdir)
assert rc.extra_update_fields['installed_collections'] == SAMPLE_ANSIBLE_DATA['installed_collections']
assert rc.extra_update_fields['ansible_version'] == '2.16.0'
assert 'event_queries_processed' not in rc.extra_update_fields
assert rc.artifacts_processed is True
@mock.patch('awx.main.tasks.callback.EventQuery')
def test_creates_event_queries_when_flag_on(self, mock_event_query, mock_me):
rc = RunnerCallback()
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'ansible_data.json')
with open(path, 'w') as f:
json.dump(SAMPLE_ANSIBLE_DATA, f)
with mock.patch('awx.main.tasks.callback.flag_enabled', return_value=True):
rc.artifacts_handler(tmpdir)
assert rc.extra_update_fields['installed_collections'] == SAMPLE_ANSIBLE_DATA['installed_collections']
assert rc.extra_update_fields['ansible_version'] == '2.16.0'
assert rc.extra_update_fields['event_queries_processed'] is False
mock_event_query.assert_called_once()
@mock.patch('awx.main.tasks.callback.EventQuery')
def test_no_event_queries_when_flag_off(self, mock_event_query, mock_me):
rc = RunnerCallback()
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'ansible_data.json')
with open(path, 'w') as f:
json.dump(SAMPLE_ANSIBLE_DATA, f)
with mock.patch('awx.main.tasks.callback.flag_enabled', return_value=False):
rc.artifacts_handler(tmpdir)
mock_event_query.assert_not_called()
def test_handles_missing_artifact_file(self, mock_me):
rc = RunnerCallback()
with tempfile.TemporaryDirectory() as tmpdir:
with mock.patch('awx.main.tasks.callback.flag_enabled', return_value=False):
rc.artifacts_handler(tmpdir)
assert 'installed_collections' not in rc.extra_update_fields
assert 'ansible_version' not in rc.extra_update_fields
assert rc.artifacts_processed is True