From bead378927ef2d40e8e3ff275d3383a2d94e63c7 Mon Sep 17 00:00:00 2001 From: Madhu Kanoor Date: Mon, 21 Aug 2023 13:43:08 -0400 Subject: [PATCH] fix: [AAP-14735] process output from a module (#564) Wraps the module into a playbook and runs the playbook via ansible runner. This gives us the ability to get the module output. https://issues.redhat.com/browse/AAP-14735 The following is a sample wrapper for a module, the module_args can be a string or it can be a dictionary. ```yaml - gather_facts: false hosts: all name: wrapper tasks: - ansible.eda.upcase: name: Fred Flintstone name: Module wrapper register: module_result - ansible.builtin.set_fact: cacheable: true module_result: '{{ module_result }}' name: save result ``` --- ansible_rulebook/builtin.py | 53 ++++++++++++++++----- ansible_rulebook/exception.py | 5 ++ ansible_rulebook/schema/ruleset_schema.json | 8 ++-- tests/e2e/test_actions.py | 2 +- tests/examples/29_run_module.yml | 13 ++--- tests/test_examples.py | 13 +++-- 6 files changed, 64 insertions(+), 30 deletions(-) diff --git a/ansible_rulebook/builtin.py b/ansible_rulebook/builtin.py index ba801d2e..aee9ff16 100644 --- a/ansible_rulebook/builtin.py +++ b/ansible_rulebook/builtin.py @@ -39,6 +39,7 @@ from .exception import ( ControllerApiException, JobTemplateNotFoundException, + MissingArtifactKeyException, PlaybookNotFoundException, PlaybookStatusNotFoundException, ShutdownException, @@ -54,6 +55,7 @@ KEY_EDA_VARS = "ansible_eda" INTERNAL_ACTION_STATUS = "successful" +MODULE_OUTPUT_KEY = "module_result" async def none( @@ -446,13 +448,9 @@ async def run_module( ) logger.info("Calling Ansible runner") - module_args_str = "" - if module_args: - for k, v in module_args.items(): - if len(module_args_str) > 0: - module_args_str += " " - module_args_str += f"{k}={v!r}" - + host_pattern = ",".join(hosts) + playbook = os.path.join(temp_dir, "wrapper.yml") + _wrap_module_in_playbook(module_name, module_args, host_pattern, playbook) if retry: retries = max(retries, 1) for i in range(retries + 1): @@ -467,11 +465,7 @@ async def run_module( event_log, job_id, temp_dir, - dict( - module=module_name, - host_pattern=",".join(hosts), - module_args=module_args_str, - ), + dict(playbook=playbook), hosts, inventory, verbosity, @@ -496,6 +490,7 @@ async def run_module( action_run_at, set_facts, post_events, + MODULE_OUTPUT_KEY, ) shutil.rmtree(temp_dir) @@ -682,6 +677,7 @@ async def post_process_runner( run_at: str, set_facts: Optional[bool] = None, post_events: Optional[bool] = None, + output_key: Optional[str] = None, ): rc = int(_get_latest_artifact(private_data_dir, "rc")) @@ -720,6 +716,17 @@ async def post_process_runner( for host_facts in glob.glob(os.path.join(fact_folder, "*")): with open(host_facts) as f: fact = json.loads(f.read()) + if output_key: + if output_key not in fact: + logger.error( + "The artifacts from the ansible-runner " + "does not have key %s", + output_key, + ) + raise MissingArtifactKeyException( + f"Missing key: {output_key} in artifacts" + ) + fact = fact[output_key] fact = _embellish_internal_event(fact, action) logger.debug("fact %s", fact) if set_facts: @@ -1081,3 +1088,25 @@ def _controller_job_url(data: dict) -> str: if "id" in data: return f"{job_template_runner.host}/#/jobs/{data['id']}/details" return "" + + +def _wrap_module_in_playbook(module_name, module_args, hosts, playbook): + module_task = { + "name": "Module wrapper", + module_name: module_args, + "register": MODULE_OUTPUT_KEY, + } + result_str = "{{ " + MODULE_OUTPUT_KEY + " }}" + set_fact_task = { + "name": "save result", + "ansible.builtin.set_fact": { + MODULE_OUTPUT_KEY: result_str, + "cacheable": True, + }, + } + tasks = [module_task, set_fact_task] + wrapper = [ + dict(name="wrapper", hosts=hosts, gather_facts=False, tasks=tasks) + ] + with open(playbook, "w") as f: + yaml.dump(wrapper, f) diff --git a/ansible_rulebook/exception.py b/ansible_rulebook/exception.py index 86a29ab7..429da342 100644 --- a/ansible_rulebook/exception.py +++ b/ansible_rulebook/exception.py @@ -156,3 +156,8 @@ class UnsupportedActionException(Exception): class InventoryNotFound(Exception): pass + + +class MissingArtifactKeyException(Exception): + + pass diff --git a/ansible_rulebook/schema/ruleset_schema.json b/ansible_rulebook/schema/ruleset_schema.json index f613c7bc..f77328df 100644 --- a/ansible_rulebook/schema/ruleset_schema.json +++ b/ansible_rulebook/schema/ruleset_schema.json @@ -351,9 +351,6 @@ "run_module": { "type": "object", "properties": { - "copy_files": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -385,7 +382,10 @@ "type": "number" }, "module_args": { - "type": "object" + "type": [ + "object", + "string" + ] }, "extra_vars": { "type": "object" diff --git a/tests/e2e/test_actions.py b/tests/e2e/test_actions.py index d028249f..05a8308c 100644 --- a/tests/e2e/test_actions.py +++ b/tests/e2e/test_actions.py @@ -173,7 +173,7 @@ def test_actions_sanity(update_environment): ), "multiple_action action failed" assert ( - len(result.stdout.splitlines()) == 56 + len(result.stdout.splitlines()) == 106 ), "unexpected output from the rulebook" diff --git a/tests/examples/29_run_module.yml b/tests/examples/29_run_module.yml index 87a6a0f8..8262055b 100644 --- a/tests/examples/29_run_module.yml +++ b/tests/examples/29_run_module.yml @@ -9,14 +9,11 @@ condition: event.i == 1 action: run_module: + post_events: True name: ansible.eda.upcase module_args: name: Fred Flintstone - - - - - - - - + - name: r2 + condition: event.message == "FRED FLINTSTONE" + action: + print_event: diff --git a/tests/test_examples.py b/tests/test_examples.py index 3894777d..c369ddc6 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -843,7 +843,7 @@ async def test_29_run_module(): event = event_log.get_nowait() assert event["type"] == "Job", "0" - for i in range(4): + for i in range(9): assert event_log.get_nowait()["type"] == "AnsibleEvent", f"0.{i}" event = event_log.get_nowait() @@ -858,6 +858,9 @@ async def test_29_run_module(): assert event["rc"] == 0, "2.1" assert event["status"] == "successful", "2.2" event = event_log.get_nowait() + assert event["type"] == "Action" + assert event["action"] == "print_event" + event = event_log.get_nowait() assert event["type"] == "Shutdown", "8" assert event_log.empty() @@ -881,14 +884,14 @@ async def test_30_run_module_missing(): event = event_log.get_nowait() assert event["type"] == "Job", "0" - for i in range(4): + for i in range(10): assert event_log.get_nowait()["type"] == "AnsibleEvent", f"0.{i}" event = event_log.get_nowait() assert event["type"] == "Action", "1" assert event["action"] == "run_module", "2" - assert event["rc"] == 2, "2.1" + assert event["rc"] == 4, "2.1" assert event["status"] == "failed", "2.2" event = event_log.get_nowait() assert event["type"] == "Shutdown", "8" @@ -914,7 +917,7 @@ async def test_31_run_module_missing_args(): event = event_log.get_nowait() assert event["type"] == "Job", "0" - for i in range(4): + for i in range(6): assert event_log.get_nowait()["type"] == "AnsibleEvent", f"0.{i}" event = event_log.get_nowait() @@ -947,7 +950,7 @@ async def test_32_run_module_fail(): event = event_log.get_nowait() assert event["type"] == "Job", "0" - for i in range(8): + for i in range(12): assert event_log.get_nowait()["type"] == "AnsibleEvent", f"0.{i}" event = event_log.get_nowait()