From 050184c2a61e208f201e5575aa0a57dcf1b47c6e Mon Sep 17 00:00:00 2001 From: Madhu Kanoor Date: Tue, 8 Aug 2023 17:02:18 -0400 Subject: [PATCH] feat: [AAP-9829] added run_workflow_template action (#543) Added support for run_workflow_template action This is similar to run_job_template. Fixes #414 https://issues.redhat.com/browse/AAP-9829 --- CHANGELOG.md | 2 + ansible_rulebook/builtin.py | 138 +++++++- ansible_rulebook/exception.py | 5 + ansible_rulebook/job_template_runner.py | 114 ++++-- ansible_rulebook/schema/ruleset_schema.json | 55 +++ docs/actions.rst | 55 ++- requirements_test.txt | 1 - tests/cassettes/test_job_template.yaml | 324 ------------------ .../test_job_template_not_exist.yaml | 67 ---- tests/data/awx_test_data.py | 79 +++++ tests/examples/46_job_template.yml | 1 + tests/examples/79_workflow_template.yml | 19 + tests/test_controller.py | 220 +++++++++--- tests/test_examples.py | 96 +++++- 14 files changed, 702 insertions(+), 474 deletions(-) delete mode 100644 tests/cassettes/test_job_template.yaml delete mode 100644 tests/cassettes/test_job_template_not_exist.yaml create mode 100644 tests/data/awx_test_data.py create mode 100644 tests/examples/79_workflow_template.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index b7584b8b..9f584da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - rulebook and Drools bracket notation syntax +- new action called run_workflow_template + ### Fixed diff --git a/ansible_rulebook/builtin.py b/ansible_rulebook/builtin.py index e25fc706..ffb56a0e 100644 --- a/ansible_rulebook/builtin.py +++ b/ansible_rulebook/builtin.py @@ -42,6 +42,7 @@ PlaybookNotFoundException, PlaybookStatusNotFoundException, ShutdownException, + WorkflowJobTemplateNotFoundException, ) from .job_template_runner import job_template_runner from .messages import Shutdown @@ -763,7 +764,6 @@ async def run_job_template( ) job_id = str(uuid.uuid4()) - await event_log.put( dict( type="Job", @@ -828,11 +828,140 @@ async def run_job_template( a_log["message"] = controller_job["error"] await event_log.put(a_log) - if set_facts or post_events: + _post_process_awx( + controller_job, set_facts, post_events, "run_job_template", ruleset + ) + + +async def run_workflow_template( + event_log, + inventory: str, + hosts: List, + variables: Dict, + project_data_file: str, + source_ruleset_name: str, + source_ruleset_uuid: str, + source_rule_name: str, + source_rule_uuid: str, + rule_run_at: str, + ruleset: str, + name: str, + organization: str, + job_args: Optional[dict] = None, + set_facts: Optional[bool] = None, + post_events: Optional[bool] = None, + verbosity: int = 0, + copy_files: Optional[bool] = False, + json_mode: Optional[bool] = False, + retries: Optional[int] = 0, + retry: Optional[bool] = False, + delay: Optional[int] = 0, + **kwargs, +): + + logger.info( + "running workflow template: %s organization: %s", name, organization + ) + logger.info("ruleset: %s, rule %s", source_ruleset_name, source_rule_name) + + hosts_limit = ",".join(hosts) + if not job_args: + job_args = {} + job_args["limit"] = hosts_limit + + job_args["extra_vars"] = _collect_extra_vars( + variables, + job_args.get("extra_vars", {}), + source_ruleset_name, + source_rule_name, + ) + + job_id = str(uuid.uuid4()) + + await event_log.put( + dict( + type="Job", + job_id=job_id, + ansible_rulebook_id=settings.identifier, + name=name, + ruleset=source_ruleset_name, + ruleset_uuid=source_ruleset_uuid, + rule=source_rule_name, + rule_uuid=source_rule_uuid, + hosts=hosts_limit, + action="run_workflow_template", + ) + ) + + if retry: + retries = max(retries, 1) + + try: + for i in range(retries + 1): + if i > 0: + if delay > 0: + await asyncio.sleep(delay) + logger.info( + "Previous run_workflow_template failed. Retry %d of %d", + i, + retries, + ) + controller_job = ( + await job_template_runner.run_workflow_job_template( + name, + organization, + job_args, + ) + ) + if controller_job["status"] != "failed": + break + except ( + ControllerApiException, + WorkflowJobTemplateNotFoundException, + ) as ex: + logger.error(ex) + controller_job = {} + controller_job["status"] = "failed" + controller_job["created"] = run_at() + controller_job["error"] = str(ex) + + a_log = dict( + type="Action", + action="run_workflow_template", + action_uuid=str(uuid.uuid4()), + activation_id=settings.identifier, + job_template_name=name, + organization=organization, + job_id=job_id, + ruleset=ruleset, + ruleset_uuid=source_ruleset_uuid, + rule=source_rule_name, + rule_uuid=source_rule_uuid, + status=controller_job["status"], + run_at=controller_job["created"], + url=_controller_job_url(controller_job), + matching_events=_get_events(variables), + rule_run_at=rule_run_at, + ) + if "error" in controller_job: + a_log["message"] = controller_job["error"] + await event_log.put(a_log) + + _post_process_awx( + controller_job, + set_facts, + post_events, + "run_workflow_template", + ruleset, + ) + + +def _post_process_awx(controller_job, set_facts, post_events, action, ruleset): + if controller_job["status"] == "successful" and (set_facts or post_events): logger.debug("set_facts") - facts = controller_job["artifacts"] + facts = controller_job.get("artifacts", None) if facts: - facts = _embellish_internal_event(facts, "run_job_template") + facts = _embellish_internal_event(facts, action) logger.debug("facts %s", facts) if set_facts: lang.assert_fact(ruleset, facts) @@ -896,6 +1025,7 @@ async def shutdown( run_playbook=run_playbook, run_module=run_module, run_job_template=run_job_template, + run_workflow_template=run_workflow_template, shutdown=shutdown, ) diff --git a/ansible_rulebook/exception.py b/ansible_rulebook/exception.py index d5437597..00b5eb21 100644 --- a/ansible_rulebook/exception.py +++ b/ansible_rulebook/exception.py @@ -138,6 +138,11 @@ class JobTemplateNotFoundException(Exception): pass +class WorkflowJobTemplateNotFoundException(Exception): + + pass + + class WebSocketExchangeException(Exception): pass diff --git a/ansible_rulebook/job_template_runner.py b/ansible_rulebook/job_template_runner.py index a0ecdca0..1956189b 100644 --- a/ansible_rulebook/job_template_runner.py +++ b/ansible_rulebook/job_template_runner.py @@ -27,13 +27,14 @@ from ansible_rulebook.exception import ( ControllerApiException, JobTemplateNotFoundException, + WorkflowJobTemplateNotFoundException, ) logger = logging.getLogger(__name__) class JobTemplateRunner: - JOB_TEMPLATE_SLUG = "/api/v2/job_templates" + UNIFIED_TEMPLATE_SLUG = "/api/v2/unified_job_templates/" CONFIG_SLUG = "/api/v2/config" JOB_COMPLETION_STATUSES = ["successful", "failed", "error", "canceled"] @@ -43,8 +44,8 @@ def __init__( self.token = token self.host = host self.verify_ssl = verify_ssl - self.refresh_delay = int( - os.environ.get("EDA_JOB_TEMPLATE_REFRESH_DELAY", 10) + self.refresh_delay = float( + os.environ.get("EDA_JOB_TEMPLATE_REFRESH_DELAY", 10.0) ) self._session = None @@ -89,65 +90,118 @@ def _sslcontext(self) -> Union[bool, ssl.SSLContext]: return ssl.create_default_context(cafile=self.verify_ssl) return False - async def _get_job_template_id(self, name: str, organization: str) -> int: - slug = f"{self.JOB_TEMPLATE_SLUG}/" + async def _get_template_obj( + self, name: str, organization: str, unified_type: str + ) -> dict: params = {"name": name} while True: - json_body = await self._get_page(slug, params) + json_body = await self._get_page( + self.UNIFIED_TEMPLATE_SLUG, params + ) for jt in json_body["results"]: if ( - jt["name"] == name - and dpath.get(jt, "summary_fields.organization.name", ".") + jt["type"] == unified_type + and jt["name"] == name + and dpath.get( + jt, + "summary_fields.organization.name", + ".", + organization, + ) == organization ): - return jt["id"] + return { + "launch": dpath.get(jt, "related.launch", ".", None), + "ask_limit_on_launch": jt["ask_limit_on_launch"], + "ask_inventory_on_launch": jt[ + "ask_inventory_on_launch" + ], + "ask_variables_on_launch": jt[ + "ask_variables_on_launch" + ], + } if json_body.get("next", None): params["page"] = params.get("page", 1) + 1 else: break - raise JobTemplateNotFoundException( - ( - f"Job template {name} in organization " - f"{organization} does not exist" - ) - ) - async def run_job_template( self, name: str, organization: str, job_params: dict, ) -> dict: - job = await self.launch(name, organization, job_params) + obj = await self._get_template_obj(name, organization, "job_template") + if not obj: + raise JobTemplateNotFoundException( + ( + f"Job template {name} in organization " + f"{organization} does not exist" + ) + ) + url = urljoin(self.host, obj["launch"]) + job = await self._launch(job_params, url) + return await self._monitor_job(job["url"]) - url = job["url"] - params = {} + async def run_workflow_job_template( + self, + name: str, + organization: str, + job_params: dict, + ) -> dict: + obj = await self._get_template_obj( + name, organization, "workflow_job_template" + ) + if not obj: + raise WorkflowJobTemplateNotFoundException( + ( + f"Workflow template {name} in organization " + f"{organization} does not exist" + ) + ) + url = urljoin(self.host, obj["launch"]) + if not obj["ask_limit_on_launch"] and "limit" in job_params: + logger.warning( + "Workflow template %s does not accept limit, removing it", name + ) + job_params.pop("limit") + if not obj["ask_variables_on_launch"] and "extra_vars" in job_params: + logger.warning( + "Workflow template %s does not accept extra vars, " + "removing it", + name, + ) + job_params.pop("extra_vars") + job = await self._launch(job_params, url) + return await self._monitor_job(job["url"]) + async def _monitor_job(self, url) -> dict: while True: # fetch and process job status - json_body = await self._get_page(url, params) - job_status = json_body["status"] - if job_status in self.JOB_COMPLETION_STATUSES: + json_body = await self._get_page(url, {}) + if json_body["status"] in self.JOB_COMPLETION_STATUSES: return json_body await asyncio.sleep(self.refresh_delay) - async def launch( - self, name: str, organization: str, job_params: dict - ) -> dict: - jt_id = await self._get_job_template_id(name, organization) - url = urljoin(self.host, f"{self.JOB_TEMPLATE_SLUG}/{jt_id}/launch/") - + async def _launch(self, job_params: dict, url: str) -> dict: + body = None try: async with self._session.post( - url, json=job_params, ssl=self._sslcontext + url, + json=job_params, + ssl=self._sslcontext, + raise_for_status=False, ) as post_response: - return json.loads(await post_response.text()) + body = json.loads(await post_response.text()) + post_response.raise_for_status() + return body except aiohttp.ClientError as e: logger.error("Error connecting to controller %s", str(e)) + if body: + logger.error("Error %s", body) raise ControllerApiException(str(e)) diff --git a/ansible_rulebook/schema/ruleset_schema.json b/ansible_rulebook/schema/ruleset_schema.json index 9be5d409..f613c7bc 100644 --- a/ansible_rulebook/schema/ruleset_schema.json +++ b/ansible_rulebook/schema/ruleset_schema.json @@ -173,6 +173,9 @@ { "$ref": "#/$defs/run-job-template-action" }, + { + "$ref": "#/$defs/run-workflow-template-action" + }, { "$ref": "#/$defs/post-event-action" }, @@ -208,6 +211,9 @@ { "$ref": "#/$defs/run-job-template-action" }, + { + "$ref": "#/$defs/run-workflow-template-action" + }, { "$ref": "#/$defs/post-event-action" }, @@ -445,6 +451,55 @@ ], "additionalProperties": false }, + "run-workflow-template-action": { + "type": "object", + "properties": { + "run_workflow_template": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "job_args": { + "type": "object" + }, + "post_events": { + "type": "boolean" + }, + "set_facts": { + "type": "boolean" + }, + "ruleset": { + "type": "string" + }, + "var_root": { + "type": "string" + }, + "retry": { + "type": "boolean" + }, + "retries": { + "type": "integer" + }, + "delay": { + "type": "integer" + } + }, + "required": [ + "name", + "organization" + ], + "additionalProperties": false + } + }, + "required": [ + "run_workflow_template" + ], + "additionalProperties": false + }, "post-event-action": { "type": "object", "properties": { diff --git a/docs/actions.rst b/docs/actions.rst index 2e2342e7..9087f264 100644 --- a/docs/actions.rst +++ b/docs/actions.rst @@ -8,6 +8,7 @@ The following actions are supported: - `run_playbook`_ - `run_module`_ - `run_job_template`_ +- `run_workflow_template`_ - `set_fact`_ - `post_event`_ - `retract_fact`_ @@ -118,19 +119,19 @@ Run a job template. - The name of the organization - Yes * - set_facts - - The artifacts from the playbook execution are inserted back into the rule set as facts + - The artifacts from the job template execution are inserted back into the rule set as facts - No * - post_events - - The artifacts from the playbook execution are inserted back into the rule set as events + - The artifacts from the job template execution are inserted back into the rule set as events - No * - ruleset - The name of the ruleset to post the event or assert the fact to, default is current rule set. - No * - retry - - If the playbook fails execution, retry it once, boolean value true|false + - If the job template fails execution, retry it once, boolean value true|false - No * - retries - - If the playbook fails execution, the number of times to retry it. An integer value + - If the job template fails execution, the number of times to retry it. An integer value - No * - delay - The retry interval, an integer value specified in seconds @@ -142,6 +143,52 @@ Run a job template. - Additional arguments sent to the job template launch API. Any answers to the survey and other extra vars should be set in nested key extra_vars. Event(s) and fact(s) will be automatically included in extra_vars too. - No +run_workflow_template +********************* + +Run a workflow template. + +.. note:: + ``--controller-url`` and ``--controller-token`` cmd options must be provided to use this action + +.. list-table:: + :widths: 25 150 10 + :header-rows: 1 + + * - Name + - Description + - Required + * - name + - The name of the workflow template + - Yes + * - organization + - The name of the organization + - Yes + * - set_facts + - The artifacts from the workflow template execution are inserted back into the rule set as facts + - No + * - post_events + - The artifacts from the workflow template execution are inserted back into the rule set as events + - No + * - ruleset + - The name of the ruleset to post the event or assert the fact to, default is current rule set. + - No + * - retry + - If the workflow template fails execution, retry it once, boolean value true|false + - No + * - retries + - If the workflow template fails execution, the number of times to retry it. An integer value + - No + * - delay + - The retry interval, an integer value specified in seconds + - No + * - var_root + - If the event is a deeply nested dictionary, the var_root can specify the key name whose value should replace the matching event value. The var_root can take a dictionary to account for data when we have multiple matching events. + - No + * - job_args + - Additional arguments sent to the workflow template launch API. Any answers to the survey and other extra vars should be set in nested key extra_vars. Event(s) and fact(s) will be automatically included in extra_vars too. + - No + post_event ********** .. list-table:: Post an event to a running rule set in the rules engine diff --git a/requirements_test.txt b/requirements_test.txt index f6773b5d..7bace146 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,6 @@ pytest-asyncio pytest-timeout pytest-xdist pytest-cov -pytest-vcr pytest-check dynaconf==3.1.11 freezegun diff --git a/tests/cassettes/test_job_template.yaml b/tests/cassettes/test_job_template.yaml deleted file mode 100644 index d0ac637f..00000000 --- a/tests/cassettes/test_job_template.yaml +++ /dev/null @@ -1,324 +0,0 @@ -interactions: -- request: - body: null - headers: - authorization: - - Bearer DUMMY - method: GET - uri: https://examples.com/api/v2/job_templates/?name=Hello+World - response: - body: - string: '{"count":1,"next":null,"previous":null,"results":[{"id":7,"type":"job_template","url":"/api/v2/job_templates/7/","related":{"created_by":"/api/v2/users/1/","modified_by":"/api/v2/users/1/","labels":"/api/v2/job_templates/7/labels/","inventory":"/api/v2/inventories/1/","project":"/api/v2/projects/6/","organization":"/api/v2/organizations/1/","credentials":"/api/v2/job_templates/7/credentials/","last_job":"/api/v2/jobs/145/","jobs":"/api/v2/job_templates/7/jobs/","schedules":"/api/v2/job_templates/7/schedules/","activity_stream":"/api/v2/job_templates/7/activity_stream/","launch":"/api/v2/job_templates/7/launch/","webhook_key":"/api/v2/job_templates/7/webhook_key/","webhook_receiver":"","notification_templates_started":"/api/v2/job_templates/7/notification_templates_started/","notification_templates_success":"/api/v2/job_templates/7/notification_templates_success/","notification_templates_error":"/api/v2/job_templates/7/notification_templates_error/","access_list":"/api/v2/job_templates/7/access_list/","survey_spec":"/api/v2/job_templates/7/survey_spec/","object_roles":"/api/v2/job_templates/7/object_roles/","instance_groups":"/api/v2/job_templates/7/instance_groups/","slice_workflow_jobs":"/api/v2/job_templates/7/slice_workflow_jobs/","copy":"/api/v2/job_templates/7/copy/"},"summary_fields":{"organization":{"id":1,"name":"Default","description":""},"inventory":{"id":1,"name":"Demo - Inventory","description":"","has_active_failures":false,"total_hosts":1,"hosts_with_active_failures":0,"total_groups":0,"has_inventory_sources":false,"total_inventory_sources":0,"inventory_sources_with_failures":0,"organization_id":1,"kind":""},"project":{"id":6,"name":"Demo - Project","description":"","status":"successful","scm_type":"git","allow_override":false},"last_job":{"id":145,"name":"Hello - World","description":"","finished":"2023-05-03T18:53:06.894495Z","status":"successful","failed":false},"last_update":{"id":145,"name":"Hello - World","description":"","status":"successful","failed":false},"created_by":{"id":1,"username":"admin","first_name":"","last_name":""},"modified_by":{"id":1,"username":"admin","first_name":"","last_name":""},"object_roles":{"admin_role":{"description":"Can - manage all aspects of the job template","name":"Admin","id":31},"execute_role":{"description":"May - run the job template","name":"Execute","id":32},"read_role":{"description":"May - view settings for the job template","name":"Read","id":33}},"user_capabilities":{"edit":true,"delete":true,"start":true,"schedule":true,"copy":true},"labels":{"count":0,"results":[]},"recent_jobs":[{"id":145,"status":"successful","finished":"2023-05-03T18:53:06.894495Z","canceled_on":null,"type":"job"},{"id":144,"status":"successful","finished":"2023-05-03T18:50:49.332330Z","canceled_on":null,"type":"job"},{"id":139,"status":"successful","finished":"2023-01-05T14:55:42.054620Z","canceled_on":null,"type":"job"},{"id":136,"status":"successful","finished":"2023-01-04T21:59:59.243420Z","canceled_on":null,"type":"job"},{"id":135,"status":"successful","finished":"2022-12-20T15:12:53.506501Z","canceled_on":null,"type":"job"},{"id":134,"status":"successful","finished":"2022-12-20T15:07:21.029528Z","canceled_on":null,"type":"job"},{"id":133,"status":"successful","finished":"2022-12-20T14:53:35.210208Z","canceled_on":null,"type":"job"},{"id":100,"status":"successful","finished":"2022-12-20T14:53:09.225410Z","canceled_on":null,"type":"job"},{"id":67,"status":"error","finished":"2022-12-20T14:42:44.967517Z","canceled_on":null,"type":"job"},{"id":66,"status":"successful","finished":"2022-12-19T21:32:48.801465Z","canceled_on":null,"type":"job"}],"credentials":[{"id":1,"name":"Demo - Credential","description":"","kind":"ssh","cloud":false}]},"created":"2022-11-17T14:50:12.370343Z","modified":"2023-05-03T18:48:45.915165Z","name":"Hello - World","description":"","job_type":"run","inventory":1,"project":6,"playbook":"hello_world.yml","scm_branch":"","forks":0,"limit":"","verbosity":0,"extra_vars":"---","job_tags":"","force_handlers":false,"skip_tags":"","start_at_task":"","timeout":0,"use_fact_cache":false,"organization":1,"last_job_run":"2023-05-03T18:53:06.894495Z","last_job_failed":false,"next_job_run":null,"status":"successful","execution_environment":null,"host_config_key":"","ask_scm_branch_on_launch":false,"ask_diff_mode_on_launch":false,"ask_variables_on_launch":true,"ask_limit_on_launch":true,"ask_tags_on_launch":false,"ask_skip_tags_on_launch":false,"ask_job_type_on_launch":false,"ask_verbosity_on_launch":false,"ask_inventory_on_launch":false,"ask_credential_on_launch":false,"ask_execution_environment_on_launch":false,"ask_labels_on_launch":false,"ask_forks_on_launch":false,"ask_job_slice_count_on_launch":false,"ask_timeout_on_launch":false,"ask_instance_groups_on_launch":false,"survey_enabled":true,"become_enabled":false,"diff_mode":false,"allow_simultaneous":false,"custom_virtualenv":null,"job_slice_count":1,"webhook_service":"","webhook_credential":null,"prevent_instance_group_fallback":false}]}' - headers: - Access-Control-Expose-Headers: - - X-API-Request-Id - Allow: - - GET, POST, HEAD, OPTIONS - Cache-Control: - - no-cache, no-store, must-revalidate - Connection: - - keep-alive - Content-Language: - - en - Content-Length: - - '5012' - Content-Type: - - application/json - Date: - - Wed, 03 May 2023 18:55:04 GMT - Expires: - - '0' - Pragma: - - no-cache - Server: - - nginx - Strict-Transport-Security: - - max-age=15768000 - Vary: - - Accept, Accept-Language, Origin, Cookie - X-API-Node: - - awx1-b89c6f9fd-4h9lr - X-API-Product-Name: - - AWX - X-API-Product-Version: - - 21.8.0 - X-API-Request-Id: - - bd8ae0932b1f48b0a7112bac75d81f21 - X-API-Time: - - 0.110s - X-API-Total-Time: - - 0.188s - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - status: - code: 200 - message: OK - url: https://examples.com/api/v2/job_templates/?name=Hello+World -- request: - body: null - headers: - authorization: - - Bearer DUMMY - method: POST - uri: https://examples.com/api/v2/job_templates/7/launch/ - response: - body: - string: '{"job":146,"ignored_fields":{},"id":146,"type":"job","url":"/api/v2/jobs/146/","related":{"created_by":"/api/v2/users/1/","modified_by":"/api/v2/users/1/","labels":"/api/v2/jobs/146/labels/","inventory":"/api/v2/inventories/1/","project":"/api/v2/projects/6/","organization":"/api/v2/organizations/1/","credentials":"/api/v2/jobs/146/credentials/","unified_job_template":"/api/v2/job_templates/7/","stdout":"/api/v2/jobs/146/stdout/","job_events":"/api/v2/jobs/146/job_events/","job_host_summaries":"/api/v2/jobs/146/job_host_summaries/","activity_stream":"/api/v2/jobs/146/activity_stream/","notifications":"/api/v2/jobs/146/notifications/","create_schedule":"/api/v2/jobs/146/create_schedule/","job_template":"/api/v2/job_templates/7/","cancel":"/api/v2/jobs/146/cancel/","relaunch":"/api/v2/jobs/146/relaunch/"},"summary_fields":{"organization":{"id":1,"name":"Default","description":""},"inventory":{"id":1,"name":"Demo - Inventory","description":"","has_active_failures":false,"total_hosts":1,"hosts_with_active_failures":0,"total_groups":0,"has_inventory_sources":false,"total_inventory_sources":0,"inventory_sources_with_failures":0,"organization_id":1,"kind":""},"project":{"id":6,"name":"Demo - Project","description":"","status":"successful","scm_type":"git","allow_override":false},"job_template":{"id":7,"name":"Hello - World","description":""},"unified_job_template":{"id":7,"name":"Hello World","description":"","unified_job_type":"job"},"created_by":{"id":1,"username":"admin","first_name":"","last_name":""},"modified_by":{"id":1,"username":"admin","first_name":"","last_name":""},"user_capabilities":{"delete":true,"start":true},"labels":{"count":0,"results":[]},"credentials":[{"id":1,"name":"Demo - Credential","description":"","kind":"ssh","cloud":false}]},"created":"2023-05-03T18:55:04.470052Z","modified":"2023-05-03T18:55:04.510245Z","name":"Hello - World","description":"","job_type":"run","inventory":1,"project":6,"playbook":"hello_world.yml","scm_branch":"","forks":0,"limit":"","verbosity":0,"extra_vars":"{}","job_tags":"","force_handlers":false,"skip_tags":"","start_at_task":"","timeout":0,"use_fact_cache":false,"organization":1,"unified_job_template":7,"launch_type":"manual","status":"pending","execution_environment":null,"failed":false,"started":null,"finished":null,"canceled_on":null,"elapsed":0.0,"job_args":"","job_cwd":"","job_env":{},"job_explanation":"","execution_node":"","controller_node":"","result_traceback":"","event_processing_finished":false,"launched_by":{"id":1,"name":"admin","type":"user","url":"/api/v2/users/1/"},"work_unit_id":null,"job_template":7,"passwords_needed_to_start":[],"allow_simultaneous":false,"artifacts":{},"scm_revision":"","instance_group":null,"diff_mode":false,"job_slice_number":0,"job_slice_count":1,"webhook_service":"","webhook_credential":null,"webhook_guid":""}' - headers: - Access-Control-Expose-Headers: - - X-API-Request-Id - Allow: - - GET, POST, HEAD, OPTIONS - Cache-Control: - - no-cache, no-store, must-revalidate - Connection: - - keep-alive - Content-Language: - - en - Content-Length: - - '2836' - Content-Type: - - application/json - Date: - - Wed, 03 May 2023 18:55:04 GMT - Expires: - - '0' - Location: - - /api/v2/jobs/146/ - Pragma: - - no-cache - Server: - - nginx - Strict-Transport-Security: - - max-age=15768000 - Vary: - - Accept, Accept-Language, Origin, Cookie - X-API-Node: - - awx1-b89c6f9fd-4h9lr - X-API-Product-Name: - - AWX - X-API-Product-Version: - - 21.8.0 - X-API-Request-Id: - - 5a3285a3a3f54034b705e2d4e0eeb830 - X-API-Time: - - 0.174s - X-API-Total-Time: - - 0.346s - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - status: - code: 201 - message: Created - url: https://examples.com/api/v2/job_templates/7/launch/ -- request: - body: null - headers: - authorization: - - Bearer DUMMY - method: GET - uri: https://examples.com/api/v2/jobs/146/ - response: - body: - string: '{"id":146,"type":"job","url":"/api/v2/jobs/146/","related":{"created_by":"/api/v2/users/1/","modified_by":"/api/v2/users/1/","labels":"/api/v2/jobs/146/labels/","inventory":"/api/v2/inventories/1/","project":"/api/v2/projects/6/","organization":"/api/v2/organizations/1/","credentials":"/api/v2/jobs/146/credentials/","unified_job_template":"/api/v2/job_templates/7/","stdout":"/api/v2/jobs/146/stdout/","job_events":"/api/v2/jobs/146/job_events/","job_host_summaries":"/api/v2/jobs/146/job_host_summaries/","activity_stream":"/api/v2/jobs/146/activity_stream/","notifications":"/api/v2/jobs/146/notifications/","create_schedule":"/api/v2/jobs/146/create_schedule/","job_template":"/api/v2/job_templates/7/","cancel":"/api/v2/jobs/146/cancel/","relaunch":"/api/v2/jobs/146/relaunch/"},"summary_fields":{"organization":{"id":1,"name":"Default","description":""},"inventory":{"id":1,"name":"Demo - Inventory","description":"","has_active_failures":false,"total_hosts":1,"hosts_with_active_failures":0,"total_groups":0,"has_inventory_sources":false,"total_inventory_sources":0,"inventory_sources_with_failures":0,"organization_id":1,"kind":""},"project":{"id":6,"name":"Demo - Project","description":"","status":"successful","scm_type":"git","allow_override":false},"job_template":{"id":7,"name":"Hello - World","description":""},"unified_job_template":{"id":7,"name":"Hello World","description":"","unified_job_type":"job"},"created_by":{"id":1,"username":"admin","first_name":"","last_name":""},"modified_by":{"id":1,"username":"admin","first_name":"","last_name":""},"user_capabilities":{"delete":true,"start":true},"labels":{"count":0,"results":[]},"credentials":[{"id":1,"name":"Demo - Credential","description":"","kind":"ssh","cloud":false}]},"created":"2023-05-03T18:55:04.470052Z","modified":"2023-05-03T18:55:04.470063Z","name":"Hello - World","description":"","job_type":"run","inventory":1,"project":6,"playbook":"hello_world.yml","scm_branch":"","forks":0,"limit":"","verbosity":0,"extra_vars":"{}","job_tags":"","force_handlers":false,"skip_tags":"","start_at_task":"","timeout":0,"use_fact_cache":false,"organization":1,"unified_job_template":7,"launch_type":"manual","status":"pending","execution_environment":null,"failed":false,"started":null,"finished":null,"canceled_on":null,"elapsed":0.0,"job_args":"","job_cwd":"","job_env":{},"job_explanation":"","execution_node":"","controller_node":"","result_traceback":"","event_processing_finished":false,"launched_by":{"id":1,"name":"admin","type":"user","url":"/api/v2/users/1/"},"work_unit_id":null,"job_template":7,"passwords_needed_to_start":[],"allow_simultaneous":false,"artifacts":{},"scm_revision":"","instance_group":null,"diff_mode":false,"job_slice_number":0,"job_slice_count":1,"webhook_service":"","webhook_credential":null,"webhook_guid":"","host_status_counts":null,"playbook_counts":{"play_count":0,"task_count":0},"custom_virtualenv":null}' - headers: - Access-Control-Expose-Headers: - - X-API-Request-Id - Allow: - - GET, DELETE, HEAD, OPTIONS - Cache-Control: - - no-cache, no-store, must-revalidate - Connection: - - keep-alive - Content-Language: - - en - Content-Length: - - '2907' - Content-Type: - - application/json - Date: - - Wed, 03 May 2023 18:55:04 GMT - Expires: - - '0' - Pragma: - - no-cache - Server: - - nginx - Strict-Transport-Security: - - max-age=15768000 - Vary: - - Accept, Accept-Language, Origin, Cookie - X-API-Node: - - awx1-b89c6f9fd-4h9lr - X-API-Product-Name: - - AWX - X-API-Product-Version: - - 21.8.0 - X-API-Request-Id: - - 02273c246a8844f2bce0b60274658ec6 - X-API-Time: - - 0.084s - X-API-Total-Time: - - 0.162s - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - status: - code: 200 - message: OK - url: https://examples.com/api/v2/jobs/146/ -- request: - body: null - headers: - authorization: - - Bearer DUMMY - method: GET - uri: https://examples.com/api/v2/jobs/146/ - response: - body: - string: '{"id":146,"type":"job","url":"/api/v2/jobs/146/","related":{"created_by":"/api/v2/users/1/","labels":"/api/v2/jobs/146/labels/","inventory":"/api/v2/inventories/1/","project":"/api/v2/projects/6/","organization":"/api/v2/organizations/1/","credentials":"/api/v2/jobs/146/credentials/","unified_job_template":"/api/v2/job_templates/7/","stdout":"/api/v2/jobs/146/stdout/","execution_environment":"/api/v2/execution_environments/1/","job_events":"/api/v2/jobs/146/job_events/","job_host_summaries":"/api/v2/jobs/146/job_host_summaries/","activity_stream":"/api/v2/jobs/146/activity_stream/","notifications":"/api/v2/jobs/146/notifications/","create_schedule":"/api/v2/jobs/146/create_schedule/","job_template":"/api/v2/job_templates/7/","cancel":"/api/v2/jobs/146/cancel/","relaunch":"/api/v2/jobs/146/relaunch/"},"summary_fields":{"organization":{"id":1,"name":"Default","description":""},"inventory":{"id":1,"name":"Demo - Inventory","description":"","has_active_failures":false,"total_hosts":1,"hosts_with_active_failures":0,"total_groups":0,"has_inventory_sources":false,"total_inventory_sources":0,"inventory_sources_with_failures":0,"organization_id":1,"kind":""},"execution_environment":{"id":1,"name":"AWX - EE (latest)","description":"","image":"quay.io/ansible/awx-ee:latest"},"project":{"id":6,"name":"Demo - Project","description":"","status":"successful","scm_type":"git","allow_override":false},"job_template":{"id":7,"name":"Hello - World","description":""},"unified_job_template":{"id":7,"name":"Hello World","description":"","unified_job_type":"job"},"instance_group":{"id":2,"name":"default","is_container_group":true},"created_by":{"id":1,"username":"admin","first_name":"","last_name":""},"user_capabilities":{"delete":true,"start":true},"labels":{"count":0,"results":[]},"credentials":[{"id":1,"name":"Demo - Credential","description":"","kind":"ssh","cloud":false}]},"created":"2023-05-03T18:55:04.470052Z","modified":"2023-05-03T18:55:04.803982Z","name":"Hello - World","description":"","job_type":"run","inventory":1,"project":6,"playbook":"hello_world.yml","scm_branch":"","forks":0,"limit":"","verbosity":0,"extra_vars":"{}","job_tags":"","force_handlers":false,"skip_tags":"","start_at_task":"","timeout":0,"use_fact_cache":false,"organization":1,"unified_job_template":7,"launch_type":"manual","status":"running","execution_environment":1,"failed":false,"started":"2023-05-03T18:55:04.938690Z","finished":null,"canceled_on":null,"elapsed":4.46897,"job_args":"[\"ansible-playbook\", - \"-u\", \"admin\", \"-i\", \"/runner/inventory/hosts\", \"-e\", \"@/runner/env/extravars\", - \"hello_world.yml\"]","job_cwd":"/runner/project","job_env":{"KUBERNETES_SERVICE_PORT_HTTPS":"443","KUBERNETES_SERVICE_PORT":"443","AWX1_SERVICE_SERVICE_HOST":"10.104.134.20","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_SERVICE_HOST":"10.100.6.11","HOSTNAME":"automation-job-146-ghjk8","PWD":"/runner","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP_PORT":"8443","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP_PROTO":"tcp","AWX1_SERVICE_PORT":"tcp://10.104.134.20:80","HOME":"/runner","KUBERNETES_PORT_443_TCP":"tcp://10.96.0.1:443","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT":"tcp://10.100.6.11:8443","AWX1_SERVICE_PORT_80_TCP_PORT":"80","AWX1_SERVICE_SERVICE_PORT":"80","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_SERVICE_PORT_HTTPS":"8443","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_SERVICE_PORT":"8443","SHLVL":"0","KUBERNETES_PORT_443_TCP_PROTO":"tcp","AWX1_SERVICE_SERVICE_PORT_HTTP":"80","KUBERNETES_PORT_443_TCP_ADDR":"10.96.0.1","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP":"tcp://10.100.6.11:8443","KUBERNETES_SERVICE_HOST":"10.96.0.1","KUBERNETES_PORT":"tcp://10.96.0.1:443","KUBERNETES_PORT_443_TCP_PORT":"443","AWX1_SERVICE_PORT_80_TCP_ADDR":"10.104.134.20","PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP_ADDR":"10.100.6.11","AWX1_SERVICE_PORT_80_TCP_PROTO":"tcp","AWX1_SERVICE_PORT_80_TCP":"tcp://10.104.134.20:80","LC_CTYPE":"C.UTF-8","ANSIBLE_FORCE_COLOR":"True","ANSIBLE_HOST_KEY_CHECKING":"False","ANSIBLE_INVENTORY_UNPARSED_FAILED":"True","ANSIBLE_PARAMIKO_RECORD_HOST_KEYS":"False","AWX_PRIVATE_DATA_DIR":"/tmp/awx_146_1bysuira","JOB_ID":"146","INVENTORY_ID":"1","PROJECT_REVISION":"347e44fea036c94d5f60e544de006453ee5c71ad","ANSIBLE_RETRY_FILES_ENABLED":"False","MAX_EVENT_RES":"700000","AWX_HOST":"https://towerhost","ANSIBLE_SSH_CONTROL_PATH_DIR":"/runner/cp","ANSIBLE_COLLECTIONS_PATHS":"/runner/requirements_collections:~/.ansible/collections:/usr/share/ansible/collections","ANSIBLE_ROLES_PATH":"/runner/requirements_roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles","ANSIBLE_CALLBACK_PLUGINS":"/usr/local/lib/python3.9/site-packages/ansible_runner/display_callback/callback","ANSIBLE_STDOUT_CALLBACK":"awx_display","AWX_ISOLATED_DATA_DIR":"/runner/artifacts/146","RUNNER_OMIT_EVENTS":"False","RUNNER_ONLY_FAILED_EVENTS":"False"},"job_explanation":"","execution_node":"","controller_node":"awx1-b89c6f9fd-4h9lr","result_traceback":"","event_processing_finished":false,"launched_by":{"id":1,"name":"admin","type":"user","url":"/api/v2/users/1/"},"work_unit_id":"uVAJNvrv","job_template":7,"passwords_needed_to_start":[],"allow_simultaneous":false,"artifacts":{},"scm_revision":"347e44fea036c94d5f60e544de006453ee5c71ad","instance_group":2,"diff_mode":false,"job_slice_number":0,"job_slice_count":1,"webhook_service":"","webhook_credential":null,"webhook_guid":"","host_status_counts":null,"playbook_counts":{"play_count":1,"task_count":1},"custom_virtualenv":null}' - headers: - Access-Control-Expose-Headers: - - X-API-Request-Id - Allow: - - GET, DELETE, HEAD, OPTIONS - Cache-Control: - - no-cache, no-store, must-revalidate - Connection: - - keep-alive - Content-Language: - - en - Content-Length: - - '5657' - Content-Type: - - application/json - Date: - - Wed, 03 May 2023 18:55:09 GMT - Expires: - - '0' - Pragma: - - no-cache - Server: - - nginx - Strict-Transport-Security: - - max-age=15768000 - Vary: - - Accept, Accept-Language, Origin, Cookie - X-API-Node: - - awx1-b89c6f9fd-4h9lr - X-API-Product-Name: - - AWX - X-API-Product-Version: - - 21.8.0 - X-API-Request-Id: - - f1fed134d16d4b2d85f660fd68eade9f - X-API-Time: - - 0.487s - X-API-Total-Time: - - 0.588s - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - status: - code: 200 - message: OK - url: https://examples.com/api/v2/jobs/146/ -- request: - body: null - headers: - authorization: - - Bearer DUMMY - method: GET - uri: https://examples.com/api/v2/jobs/146/ - response: - body: - string: '{"id":146,"type":"job","url":"/api/v2/jobs/146/","related":{"created_by":"/api/v2/users/1/","labels":"/api/v2/jobs/146/labels/","inventory":"/api/v2/inventories/1/","project":"/api/v2/projects/6/","organization":"/api/v2/organizations/1/","credentials":"/api/v2/jobs/146/credentials/","unified_job_template":"/api/v2/job_templates/7/","stdout":"/api/v2/jobs/146/stdout/","execution_environment":"/api/v2/execution_environments/1/","job_events":"/api/v2/jobs/146/job_events/","job_host_summaries":"/api/v2/jobs/146/job_host_summaries/","activity_stream":"/api/v2/jobs/146/activity_stream/","notifications":"/api/v2/jobs/146/notifications/","create_schedule":"/api/v2/jobs/146/create_schedule/","job_template":"/api/v2/job_templates/7/","cancel":"/api/v2/jobs/146/cancel/","relaunch":"/api/v2/jobs/146/relaunch/"},"summary_fields":{"organization":{"id":1,"name":"Default","description":""},"inventory":{"id":1,"name":"Demo - Inventory","description":"","has_active_failures":false,"total_hosts":1,"hosts_with_active_failures":0,"total_groups":0,"has_inventory_sources":false,"total_inventory_sources":0,"inventory_sources_with_failures":0,"organization_id":1,"kind":""},"execution_environment":{"id":1,"name":"AWX - EE (latest)","description":"","image":"quay.io/ansible/awx-ee:latest"},"project":{"id":6,"name":"Demo - Project","description":"","status":"successful","scm_type":"git","allow_override":false},"job_template":{"id":7,"name":"Hello - World","description":""},"unified_job_template":{"id":7,"name":"Hello World","description":"","unified_job_type":"job"},"instance_group":{"id":2,"name":"default","is_container_group":true},"created_by":{"id":1,"username":"admin","first_name":"","last_name":""},"user_capabilities":{"delete":true,"start":true},"labels":{"count":0,"results":[]},"credentials":[{"id":1,"name":"Demo - Credential","description":"","kind":"ssh","cloud":false}]},"created":"2023-05-03T18:55:04.470052Z","modified":"2023-05-03T18:55:04.803982Z","name":"Hello - World","description":"","job_type":"run","inventory":1,"project":6,"playbook":"hello_world.yml","scm_branch":"","forks":0,"limit":"","verbosity":0,"extra_vars":"{}","job_tags":"","force_handlers":false,"skip_tags":"","start_at_task":"","timeout":0,"use_fact_cache":false,"organization":1,"unified_job_template":7,"launch_type":"manual","status":"successful","execution_environment":1,"failed":false,"started":"2023-05-03T18:55:04.938690Z","finished":"2023-05-03T18:55:11.303892Z","canceled_on":null,"elapsed":6.365,"job_args":"[\"ansible-playbook\", - \"-u\", \"admin\", \"-i\", \"/runner/inventory/hosts\", \"-e\", \"@/runner/env/extravars\", - \"hello_world.yml\"]","job_cwd":"/runner/project","job_env":{"KUBERNETES_SERVICE_PORT_HTTPS":"443","KUBERNETES_SERVICE_PORT":"443","AWX1_SERVICE_SERVICE_HOST":"10.104.134.20","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_SERVICE_HOST":"10.100.6.11","HOSTNAME":"automation-job-146-ghjk8","PWD":"/runner","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP_PORT":"8443","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP_PROTO":"tcp","AWX1_SERVICE_PORT":"tcp://10.104.134.20:80","HOME":"/runner","KUBERNETES_PORT_443_TCP":"tcp://10.96.0.1:443","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT":"tcp://10.100.6.11:8443","AWX1_SERVICE_PORT_80_TCP_PORT":"80","AWX1_SERVICE_SERVICE_PORT":"80","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_SERVICE_PORT_HTTPS":"8443","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_SERVICE_PORT":"8443","SHLVL":"0","KUBERNETES_PORT_443_TCP_PROTO":"tcp","AWX1_SERVICE_SERVICE_PORT_HTTP":"80","KUBERNETES_PORT_443_TCP_ADDR":"10.96.0.1","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP":"tcp://10.100.6.11:8443","KUBERNETES_SERVICE_HOST":"10.96.0.1","KUBERNETES_PORT":"tcp://10.96.0.1:443","KUBERNETES_PORT_443_TCP_PORT":"443","AWX1_SERVICE_PORT_80_TCP_ADDR":"10.104.134.20","PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","AWX_OPERATOR_CONTROLLER_MANAGER_METRICS_SERVICE_PORT_8443_TCP_ADDR":"10.100.6.11","AWX1_SERVICE_PORT_80_TCP_PROTO":"tcp","AWX1_SERVICE_PORT_80_TCP":"tcp://10.104.134.20:80","LC_CTYPE":"C.UTF-8","ANSIBLE_FORCE_COLOR":"True","ANSIBLE_HOST_KEY_CHECKING":"False","ANSIBLE_INVENTORY_UNPARSED_FAILED":"True","ANSIBLE_PARAMIKO_RECORD_HOST_KEYS":"False","AWX_PRIVATE_DATA_DIR":"/tmp/awx_146_1bysuira","JOB_ID":"146","INVENTORY_ID":"1","PROJECT_REVISION":"347e44fea036c94d5f60e544de006453ee5c71ad","ANSIBLE_RETRY_FILES_ENABLED":"False","MAX_EVENT_RES":"700000","AWX_HOST":"https://towerhost","ANSIBLE_SSH_CONTROL_PATH_DIR":"/runner/cp","ANSIBLE_COLLECTIONS_PATHS":"/runner/requirements_collections:~/.ansible/collections:/usr/share/ansible/collections","ANSIBLE_ROLES_PATH":"/runner/requirements_roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles","ANSIBLE_CALLBACK_PLUGINS":"/usr/local/lib/python3.9/site-packages/ansible_runner/display_callback/callback","ANSIBLE_STDOUT_CALLBACK":"awx_display","AWX_ISOLATED_DATA_DIR":"/runner/artifacts/146","RUNNER_OMIT_EVENTS":"False","RUNNER_ONLY_FAILED_EVENTS":"False"},"job_explanation":"","execution_node":"","controller_node":"awx1-b89c6f9fd-4h9lr","result_traceback":"","event_processing_finished":true,"launched_by":{"id":1,"name":"admin","type":"user","url":"/api/v2/users/1/"},"work_unit_id":"uVAJNvrv","job_template":7,"passwords_needed_to_start":[],"allow_simultaneous":false,"artifacts":{},"scm_revision":"347e44fea036c94d5f60e544de006453ee5c71ad","instance_group":2,"diff_mode":false,"job_slice_number":0,"job_slice_count":1,"webhook_service":"","webhook_credential":null,"webhook_guid":"","host_status_counts":{"ok":1},"playbook_counts":{"play_count":1,"task_count":2},"custom_virtualenv":null}' - headers: - Access-Control-Expose-Headers: - - X-API-Request-Id - Allow: - - GET, DELETE, HEAD, OPTIONS - Cache-Control: - - no-cache, no-store, must-revalidate - Connection: - - keep-alive - Content-Language: - - en - Content-Length: - - '5686' - Content-Type: - - application/json - Date: - - Wed, 03 May 2023 18:55:13 GMT - Expires: - - '0' - Pragma: - - no-cache - Server: - - nginx - Strict-Transport-Security: - - max-age=15768000 - Vary: - - Accept, Accept-Language, Origin, Cookie - X-API-Node: - - awx1-b89c6f9fd-4h9lr - X-API-Product-Name: - - AWX - X-API-Product-Version: - - 21.8.0 - X-API-Request-Id: - - 9bd4f051408341e3b4a7316d9e220bf9 - X-API-Time: - - 0.069s - X-API-Total-Time: - - 0.303s - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - status: - code: 200 - message: OK - url: https://examples.com/api/v2/jobs/146/ -version: 1 diff --git a/tests/cassettes/test_job_template_not_exist.yaml b/tests/cassettes/test_job_template_not_exist.yaml deleted file mode 100644 index 6ae49386..00000000 --- a/tests/cassettes/test_job_template_not_exist.yaml +++ /dev/null @@ -1,67 +0,0 @@ -interactions: -- request: - body: null - headers: - authorization: - - Bearer DUMMY - method: GET - uri: https://examples.com/api/v2/job_templates/?name=Hello+World - response: - body: - string: "{\"count\":2,\"next\":null,\"previous\":null,\"results\":[{\"id\":9,\"type\":\"job_template\",\"url\":\"/api/v2/job_templates/9/\",\"related\":{\"created_by\":\"/api/v2/users/1/\",\"modified_by\":\"/api/v2/users/1/\",\"labels\":\"/api/v2/job_templates/9/labels/\",\"inventory\":\"/api/v2/inventories/1/\",\"project\":\"/api/v2/projects/8/\",\"organization\":\"/api/v2/organizations/1/\",\"credentials\":\"/api/v2/job_templates/9/credentials/\",\"last_job\":\"/api/v2/jobs/1091/\",\"jobs\":\"/api/v2/job_templates/9/jobs/\",\"schedules\":\"/api/v2/job_templates/9/schedules/\",\"activity_stream\":\"/api/v2/job_templates/9/activity_stream/\",\"launch\":\"/api/v2/job_templates/9/launch/\",\"webhook_key\":\"/api/v2/job_templates/9/webhook_key/\",\"webhook_receiver\":\"\",\"notification_templates_started\":\"/api/v2/job_templates/9/notification_templates_started/\",\"notification_templates_success\":\"/api/v2/job_templates/9/notification_templates_success/\",\"notification_templates_error\":\"/api/v2/job_templates/9/notification_templates_error/\",\"access_list\":\"/api/v2/job_templates/9/access_list/\",\"survey_spec\":\"/api/v2/job_templates/9/survey_spec/\",\"object_roles\":\"/api/v2/job_templates/9/object_roles/\",\"instance_groups\":\"/api/v2/job_templates/9/instance_groups/\",\"slice_workflow_jobs\":\"/api/v2/job_templates/9/slice_workflow_jobs/\",\"copy\":\"/api/v2/job_templates/9/copy/\"},\"summary_fields\":{\"organization\":{\"id\":1,\"name\":\"Default\",\"description\":\"\"},\"inventory\":{\"id\":1,\"name\":\"Demo - Inventory\",\"description\":\"\",\"has_active_failures\":false,\"total_hosts\":1,\"hosts_with_active_failures\":0,\"total_groups\":0,\"has_inventory_sources\":false,\"total_inventory_sources\":0,\"inventory_sources_with_failures\":0,\"organization_id\":1,\"kind\":\"\"},\"project\":{\"id\":8,\"name\":\"mkanoor\",\"description\":\"mkanoor\",\"status\":\"successful\",\"scm_type\":\"git\",\"allow_override\":false},\"last_job\":{\"id\":1091,\"name\":\"Hello - World\",\"description\":\"Hello World 490\",\"finished\":\"2023-04-17T18:05:40.839202Z\",\"status\":\"successful\",\"failed\":false},\"last_update\":{\"id\":1091,\"name\":\"Hello - World\",\"description\":\"Hello World 490\",\"status\":\"successful\",\"failed\":false},\"created_by\":{\"id\":1,\"username\":\"admin\",\"first_name\":\"\",\"last_name\":\"\"},\"modified_by\":{\"id\":1,\"username\":\"admin\",\"first_name\":\"\",\"last_name\":\"\"},\"object_roles\":{\"admin_role\":{\"description\":\"Can - manage all aspects of the job template\",\"name\":\"Admin\",\"id\":41},\"execute_role\":{\"description\":\"May - run the job template\",\"name\":\"Execute\",\"id\":42},\"read_role\":{\"description\":\"May - view settings for the job template\",\"name\":\"Read\",\"id\":43}},\"user_capabilities\":{\"edit\":true,\"delete\":true,\"start\":true,\"schedule\":true,\"copy\":true},\"labels\":{\"count\":0,\"results\":[]},\"survey\":{\"title\":\"exam\",\"description\":\"ResidentStartCareReasonKickIncreaseMakeSkillTowelSoup\uFFFD\"},\"recent_jobs\":[{\"id\":1091,\"status\":\"successful\",\"finished\":\"2023-04-17T18:05:40.839202Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1090,\"status\":\"successful\",\"finished\":\"2023-04-17T17:46:17.378778Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1089,\"status\":\"successful\",\"finished\":\"2023-04-17T17:45:18.565439Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1088,\"status\":\"successful\",\"finished\":\"2023-04-17T17:43:54.247403Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1087,\"status\":\"successful\",\"finished\":\"2023-04-17T17:43:43.955850Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1086,\"status\":\"successful\",\"finished\":\"2023-04-17T17:43:33.421700Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1082,\"status\":\"successful\",\"finished\":\"2023-04-17T17:07:52.557917Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1080,\"status\":\"successful\",\"finished\":\"2023-04-17T16:24:18.413605Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1079,\"status\":\"successful\",\"finished\":\"2023-04-17T16:24:08.648747Z\",\"canceled_on\":null,\"type\":\"job\"},{\"id\":1078,\"status\":\"successful\",\"finished\":\"2023-04-17T16:23:56.613290Z\",\"canceled_on\":null,\"type\":\"job\"}],\"credentials\":[]},\"created\":\"2021-12-13T16:23:41.200803Z\",\"modified\":\"2022-01-05T19:56:32.850511Z\",\"name\":\"Hello - World\",\"description\":\"Hello World 490\",\"job_type\":\"run\",\"inventory\":1,\"project\":8,\"playbook\":\"hello_world.yml\",\"scm_branch\":\"\",\"forks\":0,\"limit\":\"\",\"verbosity\":0,\"extra_vars\":\"---\",\"job_tags\":\"\",\"force_handlers\":false,\"skip_tags\":\"\",\"start_at_task\":\"\",\"timeout\":0,\"use_fact_cache\":false,\"organization\":1,\"last_job_run\":\"2023-04-17T18:05:40.839202Z\",\"last_job_failed\":false,\"next_job_run\":null,\"status\":\"successful\",\"execution_environment\":null,\"host_config_key\":\"\",\"ask_scm_branch_on_launch\":false,\"ask_diff_mode_on_launch\":false,\"ask_variables_on_launch\":false,\"ask_limit_on_launch\":false,\"ask_tags_on_launch\":false,\"ask_skip_tags_on_launch\":false,\"ask_job_type_on_launch\":false,\"ask_verbosity_on_launch\":false,\"ask_inventory_on_launch\":false,\"ask_credential_on_launch\":false,\"survey_enabled\":true,\"become_enabled\":false,\"diff_mode\":false,\"allow_simultaneous\":false,\"custom_virtualenv\":null,\"job_slice_count\":1,\"webhook_service\":\"\",\"webhook_credential\":null},{\"id\":12,\"type\":\"job_template\",\"url\":\"/api/v2/job_templates/12/\",\"related\":{\"created_by\":\"/api/v2/users/1/\",\"modified_by\":\"/api/v2/users/1/\",\"labels\":\"/api/v2/job_templates/12/labels/\",\"inventory\":\"/api/v2/inventories/1/\",\"project\":\"/api/v2/projects/8/\",\"organization\":\"/api/v2/organizations/1/\",\"credentials\":\"/api/v2/job_templates/12/credentials/\",\"jobs\":\"/api/v2/job_templates/12/jobs/\",\"schedules\":\"/api/v2/job_templates/12/schedules/\",\"activity_stream\":\"/api/v2/job_templates/12/activity_stream/\",\"launch\":\"/api/v2/job_templates/12/launch/\",\"webhook_key\":\"/api/v2/job_templates/12/webhook_key/\",\"webhook_receiver\":\"\",\"notification_templates_started\":\"/api/v2/job_templates/12/notification_templates_started/\",\"notification_templates_success\":\"/api/v2/job_templates/12/notification_templates_success/\",\"notification_templates_error\":\"/api/v2/job_templates/12/notification_templates_error/\",\"access_list\":\"/api/v2/job_templates/12/access_list/\",\"survey_spec\":\"/api/v2/job_templates/12/survey_spec/\",\"object_roles\":\"/api/v2/job_templates/12/object_roles/\",\"instance_groups\":\"/api/v2/job_templates/12/instance_groups/\",\"slice_workflow_jobs\":\"/api/v2/job_templates/12/slice_workflow_jobs/\",\"copy\":\"/api/v2/job_templates/12/copy/\"},\"summary_fields\":{\"organization\":{\"id\":1,\"name\":\"Default\",\"description\":\"\"},\"inventory\":{\"id\":1,\"name\":\"Demo - Inventory\",\"description\":\"\",\"has_active_failures\":false,\"total_hosts\":1,\"hosts_with_active_failures\":0,\"total_groups\":0,\"has_inventory_sources\":false,\"total_inventory_sources\":0,\"inventory_sources_with_failures\":0,\"organization_id\":1,\"kind\":\"\"},\"project\":{\"id\":8,\"name\":\"mkanoor\",\"description\":\"mkanoor\",\"status\":\"successful\",\"scm_type\":\"git\",\"allow_override\":false},\"created_by\":{\"id\":1,\"username\":\"admin\",\"first_name\":\"\",\"last_name\":\"\"},\"modified_by\":{\"id\":1,\"username\":\"admin\",\"first_name\":\"\",\"last_name\":\"\"},\"object_roles\":{\"admin_role\":{\"description\":\"Can - manage all aspects of the job template\",\"name\":\"Admin\",\"id\":50},\"execute_role\":{\"description\":\"May - run the job template\",\"name\":\"Execute\",\"id\":51},\"read_role\":{\"description\":\"May - view settings for the job template\",\"name\":\"Read\",\"id\":52}},\"user_capabilities\":{\"edit\":true,\"delete\":true,\"start\":true,\"schedule\":true,\"copy\":true},\"labels\":{\"count\":0,\"results\":[]},\"recent_jobs\":[],\"credentials\":[]},\"created\":\"2021-12-13T16:32:30.751129Z\",\"modified\":\"2021-12-13T16:32:30.751153Z\",\"name\":\"Hello - World No Survey\",\"description\":\"\",\"job_type\":\"run\",\"inventory\":1,\"project\":8,\"playbook\":\"hello_world.yml\",\"scm_branch\":\"\",\"forks\":0,\"limit\":\"\",\"verbosity\":0,\"extra_vars\":\"---\",\"job_tags\":\"\",\"force_handlers\":false,\"skip_tags\":\"\",\"start_at_task\":\"\",\"timeout\":0,\"use_fact_cache\":false,\"organization\":1,\"last_job_run\":\"2022-06-22T19:09:57.786737Z\",\"last_job_failed\":false,\"next_job_run\":null,\"status\":\"successful\",\"execution_environment\":null,\"host_config_key\":\"\",\"ask_scm_branch_on_launch\":false,\"ask_diff_mode_on_launch\":false,\"ask_variables_on_launch\":false,\"ask_limit_on_launch\":false,\"ask_tags_on_launch\":false,\"ask_skip_tags_on_launch\":false,\"ask_job_type_on_launch\":false,\"ask_verbosity_on_launch\":false,\"ask_inventory_on_launch\":false,\"ask_credential_on_launch\":false,\"survey_enabled\":false,\"become_enabled\":false,\"diff_mode\":false,\"allow_simultaneous\":false,\"custom_virtualenv\":null,\"job_slice_count\":1,\"webhook_service\":\"\",\"webhook_credential\":null}]}" - headers: - Access-Control-Expose-Headers: - - X-API-Request-Id - Allow: - - GET, POST, HEAD, OPTIONS - Cache-Control: - - no-cache, no-store, must-revalidate - Connection: - - keep-alive - Content-Language: - - en - Content-Length: - - '8205' - Content-Type: - - application/json - Date: - - Mon, 17 Apr 2023 18:14:44 GMT - Expires: - - '0' - Pragma: - - no-cache - Server: - - nginx - Vary: - - Accept, Accept-Language, Origin, Cookie - X-API-Node: - - localhost - X-API-Product-Name: - - Red Hat Ansible Automation Platform - X-API-Product-Version: - - 4.1.0 - X-API-Request-Id: - - c54d9aeafc754f439efcd06dea25e7b5 - X-API-Time: - - 0.069s - X-API-Total-Time: - - 0.109s - X-Frame-Options: - - DENY - status: - code: 200 - message: OK - url: https://examples.com/api/v2/job_templates/?name=Hello+World -version: 1 diff --git a/tests/data/awx_test_data.py b/tests/data/awx_test_data.py new file mode 100644 index 00000000..1d0cf0a2 --- /dev/null +++ b/tests/data/awx_test_data.py @@ -0,0 +1,79 @@ +UNIFIED_JOB_TEMPLATE_COUNT = 2 +ORGANIZATION_NAME = "Default" +JOB_TEMPLATE_NAME_1 = "JT1" +JOB_TEMPLATE_1_LAUNCH_SLUG = "/api/v2/job_templates/255/launch/" +JOB_TEMPLATE_2_LAUNCH_SLUG = "/api/v2/workflow_job_templates/300/launch/" + +JOB_TEMPLATE_1 = dict( + type="job_template", + name=JOB_TEMPLATE_NAME_1, + ask_limit_on_launch=False, + ask_variables_on_launch=False, + ask_inventory_on_launch=False, + related=dict(launch=JOB_TEMPLATE_1_LAUNCH_SLUG), + summary_fields=dict(organization=dict(name=ORGANIZATION_NAME)), +) + +JOB_TEMPLATE_2 = dict( + type="workflow_job_template", + name=JOB_TEMPLATE_NAME_1, + ask_limit_on_launch=False, + ask_variables_on_launch=False, + ask_inventory_on_launch=False, + related=dict(launch=JOB_TEMPLATE_2_LAUNCH_SLUG), +) + +UNIFIED_JOB_TEMPLATE_PAGE1_SLUG = ( + f"/api/v2/unified_job_templates/?name={JOB_TEMPLATE_NAME_1}" +) +UNIFIED_JOB_TEMPLATE_PAGE2_SLUG = ( + f"/api/v2/unified_job_templates/?name={JOB_TEMPLATE_NAME_1}&page=2" +) +UNIFIED_JOB_TEMPLATE_PAGE1_RESPONSE = dict( + count=UNIFIED_JOB_TEMPLATE_COUNT, + next=UNIFIED_JOB_TEMPLATE_PAGE2_SLUG, + previous=None, + results=[JOB_TEMPLATE_1], +) + +NO_JOB_TEMPLATE_PAGE1_RESPONSE = dict( + count=0, + next=None, + previous=None, + results=[], +) + +UNIFIED_JOB_TEMPLATE_PAGE2_RESPONSE = dict( + count=UNIFIED_JOB_TEMPLATE_COUNT, + previous=UNIFIED_JOB_TEMPLATE_PAGE1_SLUG, + next=None, + results=[JOB_TEMPLATE_2], +) + +JOB_STATUS_RUNNING = "running" +JOB_STATUS_FAILED = "failed" +JOB_STATUS_SUCCESSFUL = "successful" +JOB_ARTIFACTS = { + "fred": 45, + "barney": 90, +} +JOB_ID_1 = 909 +JOB_1_SLUG = f"/api/v2/jobs/{JOB_ID_1}/" +JOB_TEMPLATE_POST_RESPONSE = dict( + job=JOB_ID_1, + url=JOB_1_SLUG, + status=JOB_STATUS_SUCCESSFUL, + artifacts=JOB_ARTIFACTS, +) +JOB_1_RUNNING = dict( + job=JOB_ID_1, + url=JOB_1_SLUG, + status=JOB_STATUS_RUNNING, +) + +JOB_1_SUCCESSFUL = dict( + job=JOB_ID_1, + url=JOB_1_SLUG, + status=JOB_STATUS_SUCCESSFUL, + artifacts=JOB_ARTIFACTS, +) diff --git a/tests/examples/46_job_template.yml b/tests/examples/46_job_template.yml index cef7a84c..b9a6475a 100644 --- a/tests/examples/46_job_template.yml +++ b/tests/examples/46_job_template.yml @@ -16,3 +16,4 @@ hello: Fred retries: 1 delay: 10 + set_facts: True diff --git a/tests/examples/79_workflow_template.yml b/tests/examples/79_workflow_template.yml new file mode 100644 index 00000000..54b0daa0 --- /dev/null +++ b/tests/examples/79_workflow_template.yml @@ -0,0 +1,19 @@ +--- +- name: Test run workflow templates + hosts: all + sources: + - range: + limit: 5 + rules: + - name: "Run workflow template" + condition: event.i == 1 + action: + run_workflow_template: + name: Demo Workflow Template + job_args: + extra_vars: + hello: Fred + retries: 1 + delay: 10 + set_facts: True + organization: Default diff --git a/tests/test_controller.py b/tests/test_controller.py index 4f88a0e6..e0cd3af4 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -11,9 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import json -from pathlib import Path import pytest from aiohttp import ClientError @@ -22,68 +20,204 @@ from ansible_rulebook.exception import ( ControllerApiException, JobTemplateNotFoundException, + WorkflowJobTemplateNotFoundException, ) -from ansible_rulebook.job_template_runner import job_template_runner - -@pytest.fixture(scope="module") -def vcr_config(): - return {"filter_headers": [("authorization", "Bearer DUMMY")]} +from .data.awx_test_data import ( + JOB_1_RUNNING, + JOB_1_SLUG, + JOB_1_SUCCESSFUL, + JOB_TEMPLATE_1_LAUNCH_SLUG, + JOB_TEMPLATE_2_LAUNCH_SLUG, + JOB_TEMPLATE_NAME_1, + JOB_TEMPLATE_POST_RESPONSE, + NO_JOB_TEMPLATE_PAGE1_RESPONSE, + ORGANIZATION_NAME, + UNIFIED_JOB_TEMPLATE_PAGE1_RESPONSE, + UNIFIED_JOB_TEMPLATE_PAGE1_SLUG, + UNIFIED_JOB_TEMPLATE_PAGE2_RESPONSE, + UNIFIED_JOB_TEMPLATE_PAGE2_SLUG, +) +CONFIG_SLUG = "/api/v2/config" -@pytest.fixture(scope="module") -def vcr_cassette_dir(request): - return str(Path(f"{__file__}").parent / "cassettes") +@pytest.fixture +def mocked_job_template_runner(): + from ansible_rulebook.job_template_runner import job_template_runner -@pytest.mark.vcr() -@pytest.mark.asyncio -async def test_job_template(): - job_template_runner.host = "https://examples.com" + job_template_runner.host = "https://example.com" job_template_runner.token = "DUMMY" - job_template_runner.refresh_delay = 0.01 - job = await job_template_runner.run_job_template( - "Hello World", "Default", {"secret": "secret"} - ) - assert job["name"] == "Hello World" - assert job["status"] == "successful" + job_template_runner.refresh_delay = 0.05 + return job_template_runner -@pytest.mark.vcr() @pytest.mark.asyncio -async def test_job_template_not_exist(): - job_template_runner.host = "https://examples.com" - job_template_runner.token = "DUMMY" - with pytest.raises(JobTemplateNotFoundException): - await job_template_runner.run_job_template("Hello World", "no-org", {}) +async def test_job_template_get_config(mocked_job_template_runner): + text = json.dumps(dict(version="4.4.1")) + with aioresponses() as mocked: + mocked.get( + f"{mocked_job_template_runner.host}{CONFIG_SLUG}", + status=200, + body=text, + ) + data = await mocked_job_template_runner.get_config() + assert data["version"] == "4.4.1" @pytest.mark.asyncio -async def test_job_template_get_config(): - text = json.dumps(dict(version="4.4.1")) +async def test_job_template_get_config_error(mocked_job_template_runner): with aioresponses() as mocked: - job_template_runner.host = "https://example.com" - job_template_runner.token = "DUMMY" - mocked.get("https://example.com/api/v2/config", status=200, body=text) - data = await job_template_runner.get_config() - assert data["version"] == "4.4.1" + mocked.get( + f"{mocked_job_template_runner.host}{CONFIG_SLUG}", + exception=ClientError, + ) + with pytest.raises(ControllerApiException): + await mocked_job_template_runner.get_config() @pytest.mark.asyncio -async def test_job_template_get_config_error(): +async def test_job_template_get_config_auth_error(mocked_job_template_runner): with aioresponses() as mocked: - job_template_runner.host = "https://example.com" - job_template_runner.token = "DUMMY" - mocked.get("https://example.com/api/v2/config", exception=ClientError) + mocked.get( + f"{mocked_job_template_runner.host}{CONFIG_SLUG}", status=401 + ) with pytest.raises(ControllerApiException): - await job_template_runner.get_config() + await mocked_job_template_runner.get_config() + + +@pytest.mark.asyncio +async def test_run_job_template(mocked_job_template_runner): + with aioresponses() as mocked: + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE1_SLUG}", + status=200, + body=json.dumps(UNIFIED_JOB_TEMPLATE_PAGE1_RESPONSE), + ) + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE2_SLUG}", + status=200, + body=json.dumps(UNIFIED_JOB_TEMPLATE_PAGE2_RESPONSE), + ) + mocked.post( + f"{mocked_job_template_runner.host}" + f"{JOB_TEMPLATE_1_LAUNCH_SLUG}", + status=200, + body=json.dumps(JOB_TEMPLATE_POST_RESPONSE), + ) + mocked.get( + f"{mocked_job_template_runner.host}{JOB_1_SLUG}", + status=200, + body=json.dumps(JOB_1_RUNNING), + ) + mocked.get( + f"{mocked_job_template_runner.host}{JOB_1_SLUG}", + status=200, + body=json.dumps(JOB_1_SUCCESSFUL), + ) + data = await mocked_job_template_runner.run_job_template( + JOB_TEMPLATE_NAME_1, ORGANIZATION_NAME, {"a": 1} + ) + assert data["status"] == "successful" + assert data["artifacts"] == {"fred": 45, "barney": 90} + + +@pytest.mark.asyncio +async def test_run_workflow_template(mocked_job_template_runner): + with aioresponses() as mocked: + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE1_SLUG}", + status=200, + body=json.dumps(UNIFIED_JOB_TEMPLATE_PAGE1_RESPONSE), + ) + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE2_SLUG}", + status=200, + body=json.dumps(UNIFIED_JOB_TEMPLATE_PAGE2_RESPONSE), + ) + mocked.post( + f"{mocked_job_template_runner.host}" + f"{JOB_TEMPLATE_2_LAUNCH_SLUG}", + status=200, + body=json.dumps(JOB_TEMPLATE_POST_RESPONSE), + ) + mocked.get( + f"{mocked_job_template_runner.host}{JOB_1_SLUG}", + status=200, + body=json.dumps(JOB_1_RUNNING), + ) + mocked.get( + f"{mocked_job_template_runner.host}{JOB_1_SLUG}", + status=200, + body=json.dumps(JOB_1_SUCCESSFUL), + ) + + data = await mocked_job_template_runner.run_workflow_job_template( + JOB_TEMPLATE_NAME_1, ORGANIZATION_NAME, {"a": 1, "limit": "all"} + ) + assert data["status"] == "successful" + assert data["artifacts"] == {"fred": 45, "barney": 90} + + +@pytest.mark.asyncio +async def test_missing_job_template(mocked_job_template_runner): + with aioresponses() as mocked: + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE1_SLUG}", + status=200, + body=json.dumps(NO_JOB_TEMPLATE_PAGE1_RESPONSE), + ) + with pytest.raises(JobTemplateNotFoundException): + await mocked_job_template_runner.run_job_template( + JOB_TEMPLATE_NAME_1, ORGANIZATION_NAME, {"a": 1} + ) @pytest.mark.asyncio -async def test_job_template_get_config_auth_error(): +async def test_missing_workflow_template(mocked_job_template_runner): with aioresponses() as mocked: - job_template_runner.host = "https://example.com" - job_template_runner.token = "DUMMY" - mocked.get("https://example.com/api/v2/config", status=401) + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE1_SLUG}", + status=200, + body=json.dumps(NO_JOB_TEMPLATE_PAGE1_RESPONSE), + ) + with pytest.raises(WorkflowJobTemplateNotFoundException): + await mocked_job_template_runner.run_workflow_job_template( + JOB_TEMPLATE_NAME_1, ORGANIZATION_NAME, {"a": 1} + ) + + +@pytest.mark.asyncio +async def test_run_workflow_template_fail(mocked_job_template_runner): + with aioresponses() as mocked: + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE1_SLUG}", + status=200, + body=json.dumps(UNIFIED_JOB_TEMPLATE_PAGE1_RESPONSE), + ) + mocked.get( + f"{mocked_job_template_runner.host}" + f"{UNIFIED_JOB_TEMPLATE_PAGE2_SLUG}", + status=200, + body=json.dumps(UNIFIED_JOB_TEMPLATE_PAGE2_RESPONSE), + ) + mocked.post( + f"{mocked_job_template_runner.host}" + f"{JOB_TEMPLATE_2_LAUNCH_SLUG}", + status=400, + body=json.dumps({"msg": "Custom error message"}), + ) + with pytest.raises(ControllerApiException): - await job_template_runner.get_config() + await mocked_job_template_runner.run_workflow_job_template( + JOB_TEMPLATE_NAME_1, + ORGANIZATION_NAME, + {"a": 1, "limit": "all"}, + ) diff --git a/tests/test_examples.py b/tests/test_examples.py index 79100895..74306ee8 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -23,6 +23,7 @@ ControllerApiException, JobTemplateNotFoundException, VarsKeyMissingException, + WorkflowJobTemplateNotFoundException, ) from ansible_rulebook.job_template_runner import job_template_runner from ansible_rulebook.messages import Shutdown @@ -2089,7 +2090,9 @@ async def test_46_job_template(): queue = ruleset_queues[0][1] rs = ruleset_queues[0][0] - response_obj = dict(status="successful", id=945, created="dummy") + response_obj = dict( + status="successful", id=945, created="dummy", artifacts=dict(a=1) + ) job_template_runner.host = "https://examples.com" job_url = "https://examples.com/#/jobs/945/details" with SourceTask(rs.sources[0], "sources", {}, queue): @@ -2224,3 +2227,94 @@ async def test_78_complete_retract_fact(): event = event_log.get_nowait() assert event["type"] == "Shutdown", "7" assert event_log.empty() + + +WORKFLOW_TEMPLATE_ERRORS = [ + ("api error", ControllerApiException("api error")), + ( + "jt does not exist", + WorkflowJobTemplateNotFoundException("jt does not exist"), + ), + ("Kaboom", RuntimeError("Kaboom")), +] + + +@pytest.mark.parametrize("err_msg,err", WORKFLOW_TEMPLATE_ERRORS) +@pytest.mark.asyncio +async def test_79_workflow_job_template_exception(err_msg, err): + ruleset_queues, event_log = load_rulebook( + "examples/79_workflow_template.yml" + ) + + queue = ruleset_queues[0][1] + rs = ruleset_queues[0][0] + with SourceTask(rs.sources[0], "sources", {}, queue): + with patch( + "ansible_rulebook.builtin.job_template_runner." + "run_workflow_job_template", + side_effect=err, + ): + await run_rulesets( + event_log, + ruleset_queues, + dict(), + dict(), + ) + + while not event_log.empty(): + event = event_log.get_nowait() + if event["type"] == "Action": + action = event + + assert action["action"] == "run_workflow_template" + assert action["message"] == err_msg + required_keys = { + "action", + "action_uuid", + "activation_id", + "message", + "rule_run_at", + "run_at", + "rule", + "ruleset", + "rule_uuid", + "ruleset_uuid", + "status", + "type", + } + assert set(action.keys()).issuperset(required_keys) + + +@pytest.mark.asyncio +async def test_80_workflow_job_template(): + ruleset_queues, event_log = load_rulebook( + "examples/79_workflow_template.yml" + ) + + queue = ruleset_queues[0][1] + rs = ruleset_queues[0][0] + response_obj = dict( + status="successful", id=945, created="dummy", artifacts=dict(a=1) + ) + job_template_runner.host = "https://examples.com" + job_url = "https://examples.com/#/jobs/945/details" + with SourceTask(rs.sources[0], "sources", {}, queue): + with patch( + "ansible_rulebook.builtin.job_template_runner." + "run_workflow_job_template", + return_value=response_obj, + ): + await run_rulesets( + event_log, + ruleset_queues, + dict(), + dict(), + ) + + while not event_log.empty(): + event = event_log.get_nowait() + if event["type"] == "Action": + action = event + + assert action["url"] == job_url + assert action["action"] == "run_workflow_template"