diff --git a/lib/ansible/modules/web_infrastructure/ansible_tower/tower_workflow_launch.py b/lib/ansible/modules/web_infrastructure/ansible_tower/tower_workflow_launch.py new file mode 100644 index 0000000000..a7174ec3b2 --- /dev/null +++ b/lib/ansible/modules/web_infrastructure/ansible_tower/tower_workflow_launch.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_workflow_launch +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.8" +short_description: Run a workflow in Ansible Tower +description: + - Launch an Ansible Tower workflows. See + U(https://www.ansible.com/tower) for an overview. +options: + workflow_template: + description: + - The name of the workflow template to run. + required: True + extra_vars: + description: + - Any extra vars required to launch the job. + required: False + wait: + description: + - Wait for the workflow to complete. + required: False + default: True + type: bool + timeout: + description: + - If waiting for the workflow to complete this will abort after this + - ammount of seconds + +requirements: + - "python >= 2.6" +extends_documentation_fragment: tower +''' + +RETURN = ''' +tower_version: + description: The version of Tower we connected to + returned: If connection to Tower works + type: str + sample: '3.4.0' +job_info: + description: dictionnary containing information about the workflow executed + returned: If workflow launched + type: dict +''' + + +EXAMPLES = ''' +- name: Launch a workflow + tower_workflow_launch: + name: "Test Workflow" + delegate_to: localhost + run_once: true + register: workflow_results + +- name: Launch a Workflow with parameters without waiting + tower_workflow_launch: + workflow_template: "Test workflow" + extra_vars: "---\nmy: var" + wait: False + delegate_to: localhost + run_once: true + register: workflow_task_info +''' + +from ansible.module_utils.ansible_tower import TowerModule, tower_auth_config + +try: + import tower_cli + from tower_cli.api import client + from tower_cli.conf import settings + from tower_cli.exceptions import ServerError, ConnectionError, BadRequest, TowerCLIError +except ImportError: + pass + + +def main(): + argument_spec = dict( + workflow_template=dict(required=True), + extra_vars=dict(required=False), + wait=dict(required=False, default=True, type='bool'), + timeout=dict(required=False, default=None, type='int'), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + workflow_template = module.params.get('workflow_template') + extra_vars = module.params.get('extra_vars') + wait = module.params.get('wait') + timeout = module.params.get('timeout') + + # If we are going to use this result to return we can consider ourselfs changed + result = dict( + changed=False, + msg='initial message' + ) + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + # First we will test the connection. This will be a test for both check and run mode + # Note, we are not using the tower_check_mode method here because we want to do more than just a ping test + # If we are in check mode we also want to validate that we can find the workflow + try: + ping_result = client.get('/ping').json() + # Stuff the version into the results as an FYI + result['tower_version'] = ping_result['version'] + except(ServerError, ConnectionError, BadRequest) as excinfo: + result['msg'] = "Failed to reach Tower: {0}".format(excinfo) + module.fail_json(**result) + + # Now that we know we can connect, lets verify that we can resolve the workflow_template + try: + workflow = tower_cli.get_resource("workflow").get(**{'name': workflow_template}) + except TowerCLIError as e: + result['msg'] = "Failed to find workflow: {0}".format(e) + module.fail_json(**result) + + # Since we were able to find the workflow, if we are in check mode we can return now + if module.check_mode: + result['msg'] = "Check mode passed" + module.exit_json(**result) + + # We are no ready to run the workflow + try: + result['job_info'] = tower_cli.get_resource('workflow_job').launch( + workflow_job_template=workflow['id'], + monitor=False, + wait=wait, + timeout=timeout, + extra_vars=extra_vars + ) + if wait: + # If we were waiting for a result we will fail if the workflow failed + if result['job_info']['failed']: + result['msg'] = "Workflow execution failed" + module.fail_json(**result) + else: + module.exit_json(**result) + + # We were not waiting and there should be no way we can make it here without the workflow fired off so we can return a success + module.exit_json(**result) + + except TowerCLIError as e: + result['msg'] = "Failed to execute workflow: {0}".format(e) + module.fail_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/tower_workflow_launch/aliases b/test/integration/targets/tower_workflow_launch/aliases new file mode 100644 index 0000000000..229eebe6c9 --- /dev/null +++ b/test/integration/targets/tower_workflow_launch/aliases @@ -0,0 +1,2 @@ +cloud/tower +shippable/tower/group1 diff --git a/test/integration/targets/tower_workflow_launch/files/tower_assets.json b/test/integration/targets/tower_workflow_launch/files/tower_assets.json new file mode 100644 index 0000000000..4ebe8f8576 --- /dev/null +++ b/test/integration/targets/tower_workflow_launch/files/tower_assets.json @@ -0,0 +1,46 @@ +[ + { + "asset_relation": { + "workflow_nodes": [ + { + "name": "node0", + "unified_job_type": "job", + "success_nodes": [ + "node1" + ], + "failure_nodes": [], + "unified_job_name": "Demo Job Template", + "always_nodes": [] + }, + { + "name": "node1", + "unified_job_type": "job", + "success_nodes": [], + "failure_nodes": [], + "unified_job_name": "Demo Job Template", + "always_nodes": [] + } + ], + "roles": [ + { + "team": [], + "name": "Execute", + "user": [] + }, + { + "team": [], + "name": "Admin", + "user": [] + }, + { + "team": [], + "name": "Read", + "user": [] + } + ], + "survey_spec": {} + }, + "asset_type": "workflow", + "name": "Success Workflow" + }, +] diff --git a/test/integration/targets/tower_workflow_launch/tasks/main.yml b/test/integration/targets/tower_workflow_launch/tasks/main.yml new file mode 100644 index 0000000000..bd561e406e --- /dev/null +++ b/test/integration/targets/tower_workflow_launch/tasks/main.yml @@ -0,0 +1,78 @@ +- name: Get unified job template ID for Demo Job Template" + uri: + url: "https://{{ lookup('env', 'TOWER_HOST') }}/api/v2/unified_job_templates/?name=Demo+Job+Template" + method: GET + password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + user: "{{ lookup('env', 'TOWER_USERNAME') }}" + validate_certs: False + register: unified_job + +- name: Build workflow + uri: + url: "https://{{ lookup('env', 'TOWER_HOST') }}/api/v2/workflow_job_templates/" + body: + name: "Success Template" + variables: "---" + extra_vars: "" + body_format: 'json' + method: 'POST' + password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + status_code: 201 + user: "{{ lookup('env', 'TOWER_USERNAME') }}" + validate_certs: False + register: workflow + +- name: Add a node + uri: + url: "https://{{ lookup('env', 'TOWER_HOST') }}/api/v2/workflow_job_templates/{{ workflow.json.id }}/workflow_nodes/" + body: + credential: null + diff_mode: null + extra_data: {} + inventory: null + job_tags: null + job_type: null + limit: null + skip_tags: null + unified_job_template: "{{ unified_job.json.results[0].id }}" + verbosity: null + body_format: 'json' + method: 'POST' + password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + status_code: 201 + user: "{{ lookup('env', 'TOWER_USERNAME') }}" + validate_certs: False + register: node1 + +- name: Add a node + uri: + url: "https://{{ lookup('env', 'TOWER_HOST') }}/api/v2/workflow_job_templates/{{ workflow.json.id }}/workflow_nodes/" + body: + credential: null + diff_mode: null + extra_data: {} + inventory: null + job_tags: null + job_type: null + limit: null + skip_tags: null + unified_job_template: "{{ unified_job.json.results[0].id }}" + verbosity: null + body_format: 'json' + method: 'POST' + password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + status_code: 201 + user: "{{ lookup('env', 'TOWER_USERNAME') }}" + validate_certs: False + register: node2 + +- name: "Link nodes {{ node2.json.id }} to {{ node1.json.id }}" + uri: + url: "https://{{ lookup('env', 'TOWER_HOST') }}/api/v2/workflow_job_template_nodes/{{ node1.json.id }}/success_nodes/" + body: '{ "id": {{ node2.json.id }} }' + body_format: 'json' + method: 'POST' + password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + status_code: 204 + user: "{{ lookup('env', 'TOWER_USERNAME') }}" + validate_certs: False diff --git a/test/integration/targets/tower_workflow_launch/tests/validate.yml b/test/integration/targets/tower_workflow_launch/tests/validate.yml new file mode 100644 index 0000000000..846ec7592c --- /dev/null +++ b/test/integration/targets/tower_workflow_launch/tests/validate.yml @@ -0,0 +1,99 @@ +- name: Run a workflow with no parameters + tower_workflow_launch: + tower_verify_ssl: False + ignore_errors: true + register: result1 + +- assert: + that: + - result1.failed + - "'missing required arguments' in result1.msg" + +- name: Fail no connect to Tower server + tower_workflow_launch: + tower_host: 127.0.0.1:22 + tower_verify_ssl: False + workflow_template: "Here" + ignore_errors: True + register: result2 + +- assert: + that: + - result2.failed + - "'Failed to reach Tower' in result2.msg" + +- name: Connect to Tower server but request an invalid workflow + tower_workflow_launch: + tower_verify_ssl: False + workflow_template: "Does Not Exist" + ignore_errors: true + register: result3 + +- assert: + that: + - result3.failed + - "'The requested object could not be found' in result3.msg" + +- name: Connect to Tower in check_mode with a valid workflow name + tower_workflow_launch: + tower_verify_ssl: False + workflow_template: "Success Workflow" + check_mode: True + ignore_errors: true + register: result4 + +- assert: + that: + - not result4.failed + - "'Check mode passed' in result4.msg" + +- name: Connect to Tower in check_mode with a valid workflow id + tower_workflow_launch: + tower_verify_ssl: False + workflow_template: 9999999 + check_mode: True + ignore_errors: true + register: result5 + +- assert: + that: + - result5.failed + - "'The requested object could not be found' in result5.msg" + +- name: Run the workflow without waiting (this should just give us back a job ID) + tower_workflow_launch: + tower_verify_ssl: False + workflow_template: "Success Workflow" + wait: False + ignore_errors: True + register: result6 + +- assert: + that: + - not result6.failed + - "'id' in result6['job_info']" + +- name: Kick off a workflow and wait for it + tower_workflow_launch: + tower_verify_ssl: False + workflow_template: "Success Workflow" + ignore_errors: True + register: result7 + +- assert: + that: + - not result7.failed + - "'id' in result7['job_info']" + +- name: Kick off a workflow and wait for it, but only for a second + tower_workflow_launch: + tower_verify_ssl: False + workflow_template: "Success Workflow" + timeout: 1 + ignore_errors: True + register: result8 + +- assert: + that: + - result8.failed + - "'Monitoring aborted due to timeout' in result8.msg"