#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2021, Phillipe Smith <phsmithcc@gmail.com> # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' --- module: rundeck_job_run short_description: Run a Rundeck job description: - This module runs a Rundeck job specified by ID. author: "Phillipe Smith (@phsmith)" version_added: 3.8.0 options: job_id: type: str description: - The job unique ID. required: true job_options: type: dict description: - The job options for the steps. - Numeric values must be quoted. filter_nodes: type: str description: - Filter the nodes where the jobs must run. - See U(https://docs.rundeck.com/docs/manual/11-node-filters.html#node-filter-syntax). run_at_time: type: str description: - Schedule the job execution to run at specific date and time. - ISO-8601 date and time format like C(2021-10-05T15:45:00-03:00). loglevel: type: str description: - Log level configuration. choices: [debug, verbose, info, warn, error] default: info wait_execution: type: bool description: - Wait until the job finished the execution. default: true wait_execution_delay: type: int description: - Delay, in seconds, between job execution status check requests. default: 5 wait_execution_timeout: type: int description: - Job execution wait timeout in seconds. - If the timeout is reached, the job will be aborted. - Keep in mind that there is a sleep based on I(wait_execution_delay) after each job status check. default: 120 abort_on_timeout: type: bool description: - Send a job abort request if exceeded the I(wait_execution_timeout) specified. default: false extends_documentation_fragment: - community.general.rundeck - url ''' EXAMPLES = ''' - name: Run a Rundeck job community.general.rundeck_job_run: url: "https://rundeck.example.org" api_version: 39 api_token: "mytoken" job_id: "xxxxxxxxxxxxxxxxx" register: rundeck_job_run - name: Show execution info ansible.builtin.debug: var: rundeck_job_run.execution_info - name: Run a Rundeck job with options community.general.rundeck_job_run: url: "https://rundeck.example.org" api_version: 39 api_token: "mytoken" job_id: "xxxxxxxxxxxxxxxxx" job_options: option_1: "value_1" option_2: "value_3" option_3: "value_3" register: rundeck_job_run - name: Run a Rundeck job with timeout, delay between status check and abort on timeout community.general.rundeck_job_run: url: "https://rundeck.example.org" api_version: 39 api_token: "mytoken" job_id: "xxxxxxxxxxxxxxxxx" wait_execution_timeout: 30 wait_execution_delay: 10 abort_on_timeout: true register: rundeck_job_run - name: Schedule a Rundeck job community.general.rundeck_job_run: url: "https://rundeck.example.org" api_version: 39 api_token: "mytoken" job_id: "xxxxxxxxxxxxxxxxx" run_at_time: "2021-10-05T15:45:00-03:00" register: rundeck_job_schedule - name: Fire-and-forget a Rundeck job community.general.rundeck_job_run: url: "https://rundeck.example.org" api_version: 39 api_token: "mytoken" job_id: "xxxxxxxxxxxxxxxxx" wait_execution: false register: rundeck_job_run ''' RETURN = ''' execution_info: description: Rundeck job execution metadata. returned: always type: dict sample: { "msg": "Job execution succeeded!", "execution_info": { "id": 1, "href": "https://rundeck.example.org/api/39/execution/1", "permalink": "https://rundeck.example.org/project/myproject/execution/show/1", "status": "succeeded", "project": "myproject", "executionType": "user", "user": "admin", "date-started": { "unixtime": 1633449020784, "date": "2021-10-05T15:50:20Z" }, "date-ended": { "unixtime": 1633449026358, "date": "2021-10-05T15:50:26Z" }, "job": { "id": "697af0c4-72d3-4c15-86a3-b5bfe3c6cb6a", "averageDuration": 4917, "name": "Test", "group": "", "project": "myproject", "description": "", "options": { "exit_code": "0" }, "href": "https://rundeck.example.org/api/39/job/697af0c4-72d3-4c15-86a3-b5bfe3c6cb6a", "permalink": "https://rundeck.example.org/project/myproject/job/show/697af0c4-72d3-4c15-86a3-b5bfe3c6cb6a" }, "description": "sleep 5 && echo 'Test!' && exit ${option.exit_code}", "argstring": "-exit_code 0", "serverUUID": "5b9a1438-fa3a-457e-b254-8f3d70338068", "successfulNodes": [ "localhost" ], "output": "Test!" } } ''' # Modules import import json from datetime import datetime, timedelta from time import sleep from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.six.moves.urllib.parse import quote from ansible_collections.community.general.plugins.module_utils.rundeck import ( api_argument_spec, api_request ) class RundeckJobRun(object): def __init__(self, module): self.module = module self.url = self.module.params["url"] self.api_version = self.module.params["api_version"] self.job_id = self.module.params["job_id"] self.job_options = self.module.params["job_options"] or {} self.filter_nodes = self.module.params["filter_nodes"] or "" self.run_at_time = self.module.params["run_at_time"] or "" self.loglevel = self.module.params["loglevel"].upper() self.wait_execution = self.module.params['wait_execution'] self.wait_execution_delay = self.module.params['wait_execution_delay'] self.wait_execution_timeout = self.module.params['wait_execution_timeout'] self.abort_on_timeout = self.module.params['abort_on_timeout'] for k, v in self.job_options.items(): if not isinstance(v, str): self.module.exit_json( msg="Job option '%s' value must be a string" % k, execution_info={} ) def job_status_check(self, execution_id): response = dict() timeout = False due = datetime.now() + timedelta(seconds=self.wait_execution_timeout) while not timeout: endpoint = "execution/%d" % execution_id response = api_request(module=self.module, endpoint=endpoint)[0] output = api_request(module=self.module, endpoint="execution/%d/output" % execution_id) log_output = "\n".join([x["log"] for x in output[0]["entries"]]) response.update({"output": log_output}) if response["status"] == "aborted": break elif response["status"] == "scheduled": self.module.exit_json(msg="Job scheduled to run at %s" % self.run_at_time, execution_info=response, changed=True) elif response["status"] == "failed": self.module.fail_json(msg="Job execution failed", execution_info=response) elif response["status"] == "succeeded": self.module.exit_json(msg="Job execution succeeded!", execution_info=response) if datetime.now() >= due: timeout = True break # Wait for 5s before continue sleep(self.wait_execution_delay) response.update({"timed_out": timeout}) return response def job_run(self): response, info = api_request( module=self.module, endpoint="job/%s/run" % quote(self.job_id), method="POST", data={ "loglevel": self.loglevel, "options": self.job_options, "runAtTime": self.run_at_time, "filter": self.filter_nodes } ) if info["status"] != 200: self.module.fail_json(msg=info["msg"]) if not self.wait_execution: self.module.exit_json(msg="Job run send successfully!", execution_info=response) job_status = self.job_status_check(response["id"]) if job_status["timed_out"]: if self.abort_on_timeout: api_request( module=self.module, endpoint="execution/%s/abort" % response['id'], method="GET" ) abort_status = self.job_status_check(response["id"]) self.module.fail_json(msg="Job execution aborted due the timeout specified", execution_info=abort_status) self.module.fail_json(msg="Job execution timed out", execution_info=job_status) def main(): argument_spec = api_argument_spec() argument_spec.update(dict( job_id=dict(required=True, type="str"), job_options=dict(type="dict"), filter_nodes=dict(type="str"), run_at_time=dict(type="str"), wait_execution=dict(type="bool", default=True), wait_execution_delay=dict(type="int", default=5), wait_execution_timeout=dict(type="int", default=120), abort_on_timeout=dict(type="bool", default=False), loglevel=dict( type="str", choices=["debug", "verbose", "info", "warn", "error"], default="info" ) )) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=False ) if module.params["api_version"] < 14: module.fail_json(msg="API version should be at least 14") rundeck = RundeckJobRun(module) rundeck.job_run() if __name__ == "__main__": main()