Skip to content

Commit f094e15

Browse files
committed
Successfully test collection of event_query.yml data (ansible#15761)
* Callback plugin method from cmeyers adapted to global collection list Get tests passing Mild rebranding Put behind feature flag, flip true in dev Add noqa flag * Add missing wait_for_events
1 parent 589b8f4 commit f094e15

File tree

4 files changed

+148
-17
lines changed

4 files changed

+148
-17
lines changed

awx/main/tasks/jobs.py

+37-16
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
from rest_framework.exceptions import PermissionDenied
8787
from django.utils.translation import gettext_lazy as _
8888

89+
# Django flags
90+
from flags.state import flag_enabled
91+
8992
logger = logging.getLogger('awx.main.tasks.jobs')
9093

9194

@@ -439,20 +442,17 @@ def final_run_hook(self, instance, status, private_data_dir):
439442
Hook for any steps to run after job/task is marked as complete.
440443
"""
441444
instance.log_lifecycle("finalize_run")
442-
artifact_dir = os.path.join(private_data_dir, 'artifacts', str(self.instance.id))
443-
collections_info = os.path.join(artifact_dir, 'collections.json')
444-
ansible_version_file = os.path.join(artifact_dir, 'ansible_version.txt')
445-
446-
if os.path.exists(collections_info):
447-
with open(collections_info) as ee_json_info:
448-
ee_collections_info = json.loads(ee_json_info.read())
449-
instance.installed_collections = ee_collections_info
450-
instance.save(update_fields=['installed_collections'])
451-
if os.path.exists(ansible_version_file):
452-
with open(ansible_version_file) as ee_ansible_info:
453-
ansible_version_info = ee_ansible_info.readline()
454-
instance.ansible_version = ansible_version_info
455-
instance.save(update_fields=['ansible_version'])
445+
if flag_enabled("FEATURE_INDIRECT_NODE_COUNTING_ENABLED"):
446+
artifact_dir = os.path.join(private_data_dir, 'artifacts', str(self.instance.id))
447+
data_file_path = os.path.join(artifact_dir, 'ansible_data.json')
448+
449+
if os.path.exists(data_file_path):
450+
with open(data_file_path) as f:
451+
collected_data = json.loads(f.read())
452+
453+
instance.installed_collections = collected_data['installed_collections']
454+
instance.ansible_version = collected_data['ansible_version']
455+
instance.save(update_fields=['installed_collections', 'ansible_version'])
456456

457457
# Run task manager appropriately for speculative dependencies
458458
if instance.unifiedjob_blocked_jobs.exists():
@@ -927,11 +927,16 @@ def build_env(self, job, private_data_dir, private_data_files=None):
927927
if authorize:
928928
env['ANSIBLE_NET_AUTH_PASS'] = network_cred.get_input('authorize_password', default='')
929929

930-
path_vars = (
930+
path_vars = [
931931
('ANSIBLE_COLLECTIONS_PATHS', 'collections_paths', 'requirements_collections', '~/.ansible/collections:/usr/share/ansible/collections'),
932932
('ANSIBLE_ROLES_PATH', 'roles_path', 'requirements_roles', '~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles'),
933933
('ANSIBLE_COLLECTIONS_PATH', 'collections_path', 'requirements_collections', '~/.ansible/collections:/usr/share/ansible/collections'),
934-
)
934+
]
935+
936+
if flag_enabled("FEATURE_INDIRECT_NODE_COUNTING_ENABLED"):
937+
path_vars.append(
938+
('ANSIBLE_CALLBACK_PLUGINS', 'callback_plugins', 'plugins_path', '~/.ansible/plugins:/plugins/callback:/usr/share/ansible/plugins/callback'),
939+
)
935940

936941
config_values = read_ansible_config(os.path.join(private_data_dir, 'project'), list(map(lambda x: x[1], path_vars)))
937942

@@ -948,6 +953,11 @@ def build_env(self, job, private_data_dir, private_data_files=None):
948953
paths = [os.path.join(CONTAINER_ROOT, folder)] + paths
949954
env[env_key] = os.pathsep.join(paths)
950955

956+
if flag_enabled("FEATURE_INDIRECT_NODE_COUNTING_ENABLED"):
957+
env['ANSIBLE_CALLBACKS_ENABLED'] = 'indirect_instance_count'
958+
if 'callbacks_enabled' in config_values:
959+
env['ANSIBLE_CALLBACKS_ENABLED'] += ':' + config_values['callbacks_enabled']
960+
951961
return env
952962

953963
def build_args(self, job, private_data_dir, passwords):
@@ -1388,6 +1398,17 @@ def make_local_copy(project, job_private_data_dir):
13881398
shutil.copytree(cache_subpath, dest_subpath, symlinks=True)
13891399
logger.debug('{0} {1} prepared {2} from cache'.format(type(project).__name__, project.pk, dest_subpath))
13901400

1401+
if flag_enabled("FEATURE_INDIRECT_NODE_COUNTING_ENABLED"):
1402+
# copy the special callback (not stdout type) plugin to get list of collections
1403+
pdd_plugins_path = os.path.join(job_private_data_dir, 'plugins_path')
1404+
if not os.path.exists(pdd_plugins_path):
1405+
os.mkdir(pdd_plugins_path)
1406+
from awx.playbooks import library
1407+
1408+
plugin_file_source = os.path.join(library.__path__._path[0], 'indirect_instance_count.py')
1409+
plugin_file_dest = os.path.join(pdd_plugins_path, 'indirect_instance_count.py')
1410+
shutil.copyfile(plugin_file_source, plugin_file_dest)
1411+
13911412
def post_run_hook(self, instance, status):
13921413
super(RunProjectUpdate, self).post_run_hook(instance, status)
13931414
# To avoid hangs, very important to release lock even if errors happen here

awx/main/tests/live/tests/projects/test_indirect_host_counting.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1+
import yaml
2+
3+
from awx.main.tests.live.tests.conftest import wait_for_events
4+
15
from awx.main.tasks.host_indirect import build_indirect_host_data
26
from awx.main.models import Job
37

48

59
def test_indirect_host_counting(live_tmp_folder, run_job_from_playbook):
610
run_job_from_playbook('test_indirect_host_counting', 'run_task.yml', scm_url=f'file://{live_tmp_folder}/test_host_query')
711
job = Job.objects.filter(name__icontains='test_indirect_host_counting').order_by('-created').first()
12+
wait_for_events(job) # We must wait for events because system tasks iterate on job.job_events.filter(...)
813

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

1319
# Run the task logic directly with local data
1420
results = build_indirect_host_data(job, event_query)
@@ -18,3 +24,12 @@ def test_indirect_host_counting(live_tmp_folder, run_job_from_playbook):
1824
# Asserts on data that will match to the input jq string from above
1925
assert host_audit_entry.canonical_facts == {'host_name': 'foo_host_default'}
2026
assert host_audit_entry.facts == {'device_type': 'Fake Host'}
27+
28+
# Test collection of data
29+
assert 'demo.query' in job.installed_collections
30+
assert 'host_query' in job.installed_collections['demo.query']
31+
hq_text = job.installed_collections['demo.query']['host_query']
32+
hq_data = yaml.safe_load(hq_text)
33+
assert hq_data == {'demo.query.example': module_jq_str}
34+
35+
assert job.ansible_version
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# (C) 2012, Michael DeHaan, <[email protected]>
2+
# (c) 2017 Ansible Project
3+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4+
5+
from __future__ import absolute_import, division, print_function
6+
7+
__metaclass__ = type
8+
9+
10+
DOCUMENTATION = '''
11+
callback: host_query
12+
type: notification
13+
short_description: for demo of indirect host data and counting, this produces collection data
14+
version_added: historical
15+
description:
16+
- Saves collection data to artifacts folder
17+
requirements:
18+
- Whitelist in configuration
19+
- Set AWX_ISOLATED_DATA_DIR, AWX will do this
20+
'''
21+
22+
import os
23+
import json
24+
25+
from ansible.plugins.callback import CallbackBase
26+
27+
28+
# NOTE: in Ansible 1.2 or later general logging is available without
29+
# this plugin, just set ANSIBLE_LOG_PATH as an environment variable
30+
# or log_path in the DEFAULTS section of your ansible configuration
31+
# file. This callback is an example of per hosts logging for those
32+
# that want it.
33+
34+
35+
# Taken from https://github.com/ansible/ansible/blob/devel/lib/ansible/cli/galaxy.py#L1624
36+
37+
from ansible.cli.galaxy import with_collection_artifacts_manager
38+
from ansible.release import __version__
39+
40+
from ansible.galaxy.collection import find_existing_collections
41+
from ansible.utils.collection_loader import AnsibleCollectionConfig
42+
import ansible.constants as C
43+
44+
from ansible.module_utils.common.text.converters import to_text
45+
46+
47+
@with_collection_artifacts_manager
48+
def list_collections(artifacts_manager=None):
49+
artifacts_manager.require_build_metadata = False
50+
51+
default_collections_path = set(C.COLLECTIONS_PATHS)
52+
collections_search_paths = default_collections_path | set(AnsibleCollectionConfig.collection_paths)
53+
collections = list(find_existing_collections(list(collections_search_paths), artifacts_manager, dedupe=False))
54+
return collections
55+
56+
57+
class CallbackModule(CallbackBase):
58+
"""
59+
logs playbook results, per host, in /var/log/ansible/hosts
60+
"""
61+
62+
CALLBACK_VERSION = 2.0
63+
CALLBACK_TYPE = 'notification'
64+
CALLBACK_NAME = 'indirect_instance_count'
65+
CALLBACK_NEEDS_WHITELIST = True
66+
67+
TIME_FORMAT = "%b %d %Y %H:%M:%S"
68+
MSG_FORMAT = "%(now)s - %(category)s - %(data)s\n\n"
69+
70+
def v2_playbook_on_stats(self, stats):
71+
artifact_dir = os.getenv('AWX_ISOLATED_DATA_DIR')
72+
if not artifact_dir:
73+
raise RuntimeError('Only suitable in AWX, did not find private_data_dir')
74+
75+
collections_print = {}
76+
for collection_obj in list_collections():
77+
collection_print = {
78+
'version': collection_obj.ver,
79+
}
80+
host_query_path = os.path.join(to_text(collection_obj.src), 'meta', 'event_query.yml')
81+
if os.path.exists(host_query_path):
82+
with open(host_query_path, 'r') as f:
83+
collection_print['host_query'] = f.read()
84+
collections_print[collection_obj.fqcn] = collection_print
85+
86+
ansible_data = {'installed_collections': collections_print, 'ansible_version': __version__}
87+
88+
write_path = os.path.join(artifact_dir, 'ansible_data.json')
89+
with open(write_path, "w") as fd:
90+
fd.write(json.dumps(ansible_data, indent=2))
91+
92+
super().v2_playbook_on_stats(stats)

awx/settings/development.py

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767

6868
AWX_CALLBACK_PROFILE = True
6969

70+
# this modifies FLAGS set by defaults
71+
FLAGS['FEATURE_INDIRECT_NODE_COUNTING_ENABLED'] = [{'condition': 'boolean', 'value': True}] # noqa
72+
7073
# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!=================================
7174
# Disable normal scheduled/triggered task managers (DependencyManager, TaskManager, WorkflowManager).
7275
# Allows user to trigger task managers directly for debugging and profiling purposes.

0 commit comments

Comments
 (0)