mirror of
https://github.com/ansible/awx.git
synced 2026-06-18 21:27:43 -02:30
* [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>
157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
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 _
|
|
|
|
|
|
def test_delay_update(mock_me):
|
|
rc = RunnerCallback()
|
|
rc.delay_update(foo='bar')
|
|
assert rc.extra_update_fields == {'foo': 'bar'}
|
|
rc.delay_update(foo='foobar')
|
|
assert rc.extra_update_fields == {'foo': 'foobar'}
|
|
rc.delay_update(bar='foo')
|
|
assert rc.get_delayed_update_fields() == {'foo': 'foobar', 'bar': 'foo', 'emitted_events': 0}
|
|
|
|
|
|
def test_delay_update_skip_if_set(mock_me):
|
|
rc = RunnerCallback()
|
|
rc.delay_update(foo='bar', skip_if_already_set=True)
|
|
assert rc.extra_update_fields == {'foo': 'bar'}
|
|
rc.delay_update(foo='foobar', skip_if_already_set=True)
|
|
assert rc.extra_update_fields == {'foo': 'bar'}
|
|
|
|
|
|
def test_delay_update_failure_fields(mock_me):
|
|
rc = RunnerCallback()
|
|
rc.delay_update(job_explanation='1')
|
|
rc.delay_update(job_explanation=_('2'))
|
|
assert rc.extra_update_fields == {'job_explanation': '1\n2'}
|
|
rc.delay_update(result_traceback='1')
|
|
rc.delay_update(result_traceback=_('2'))
|
|
rc.delay_update(result_traceback=_('3'), skip_if_already_set=True)
|
|
assert rc.extra_update_fields == {'job_explanation': '1\n2', 'result_traceback': '1\n2'}
|
|
|
|
|
|
def test_duplicate_updates(mock_me):
|
|
rc = RunnerCallback()
|
|
rc.delay_update(job_explanation='really long summary...')
|
|
rc.delay_update(job_explanation='really long summary...')
|
|
rc.delay_update(job_explanation='really long summary...')
|
|
assert rc.extra_update_fields == {'job_explanation': 'really long summary...'}
|
|
|
|
|
|
def test_special_ansible_runner_message(mock_me):
|
|
rc = RunnerCallback()
|
|
rc.delay_update(result_traceback='Traceback:\ngot an unexpected keyword argument\nFile: foo.py')
|
|
rc.delay_update(result_traceback='Traceback:\ngot an unexpected keyword argument\nFile: bar.py')
|
|
assert rc.get_delayed_update_fields().get('result_traceback') == (
|
|
'Traceback:\ngot an unexpected keyword argument\nFile: foo.py\n'
|
|
'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
|