From 9772485d3cb3b728e9a3b15d5b2d65aab5929593 Mon Sep 17 00:00:00 2001 From: Phillipe Smith Date: Mon, 11 Oct 2021 01:55:47 -0300 Subject: [PATCH] Add new modules rundeck_job_run and rundeck_job_executions_info (#3521) * Add new module rundeck_job_run * Add new module rundeck_job_executions_info * Removed supports_check_mode * Fix supports_check_mode * Fix version_added * Fixes for PR#3521 * Fix default value for loglevel in the doc * Fix job_status_check loop * Add proposed changes in PR#3521 * Add proposed changes in PR#3521 * Change executions_info output to executions * Add rundeck integration tests * Fix rundeck integration test * Add more tests to rundeck integration tests * Update job_options doc * Add more tests to rundeck integration tests * Add more examples to rundeck_job_run doc * Add proposed fixes for PR#3521 * Add proposed fixes for PR#3521 * Fix job_options * Add proposed changes for PR#3521 --- .github/BOTMETA.yml | 4 + plugins/doc_fragments/rundeck.py | 31 ++ plugins/module_utils/rundeck.py | 94 ++++++ .../modules/rundeck_job_executions_info.py | 1 + plugins/modules/rundeck_job_run.py | 1 + .../rundeck_job_executions_info.py | 193 +++++++++++ .../web_infrastructure/rundeck_job_run.py | 317 ++++++++++++++++++ tests/integration/targets/rundeck/aliases | 8 + .../targets/rundeck/defaults/main.yml | 3 + .../targets/rundeck/files/test_job.yaml | 23 ++ .../integration/targets/rundeck/meta/main.yml | 2 + .../targets/rundeck/tasks/main.yml | 123 +++++++ .../targets/setup_rundeck/defaults/main.yml | 2 + .../targets/setup_rundeck/meta/main.yml | 0 .../targets/setup_rundeck/tasks/main.yml | 37 ++ .../targets/setup_rundeck/vars/Debian.yml | 1 + .../targets/setup_rundeck/vars/RedHat.yml | 1 + 17 files changed, 841 insertions(+) create mode 100644 plugins/doc_fragments/rundeck.py create mode 100644 plugins/module_utils/rundeck.py create mode 120000 plugins/modules/rundeck_job_executions_info.py create mode 120000 plugins/modules/rundeck_job_run.py create mode 100644 plugins/modules/web_infrastructure/rundeck_job_executions_info.py create mode 100644 plugins/modules/web_infrastructure/rundeck_job_run.py create mode 100644 tests/integration/targets/rundeck/aliases create mode 100644 tests/integration/targets/rundeck/defaults/main.yml create mode 100644 tests/integration/targets/rundeck/files/test_job.yaml create mode 100644 tests/integration/targets/rundeck/meta/main.yml create mode 100644 tests/integration/targets/rundeck/tasks/main.yml create mode 100644 tests/integration/targets/setup_rundeck/defaults/main.yml create mode 100644 tests/integration/targets/setup_rundeck/meta/main.yml create mode 100644 tests/integration/targets/setup_rundeck/tasks/main.yml create mode 100644 tests/integration/targets/setup_rundeck/vars/Debian.yml create mode 100644 tests/integration/targets/setup_rundeck/vars/RedHat.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index b4aedd856c..f7aae7f272 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1163,6 +1163,10 @@ files: maintainers: nerzhul $modules/web_infrastructure/rundeck_project.py: maintainers: nerzhul + $modules/web_infrastructure/rundeck_job_run.py: + maintainers: phsmith + $modules/web_infrastructure/rundeck_job_executions_info.py: + maintainers: phsmith $modules/web_infrastructure/sophos_utm/: maintainers: $team_e_spirit keywords: sophos utm diff --git a/plugins/doc_fragments/rundeck.py b/plugins/doc_fragments/rundeck.py new file mode 100644 index 0000000000..056a54f37f --- /dev/null +++ b/plugins/doc_fragments/rundeck.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Phillipe Smith +# 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 + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +options: + url: + type: str + description: + - Rundeck instance URL. + required: true + api_version: + type: int + description: + - Rundeck API version to be used. + - API version must be at least 14. + default: 39 + api_token: + type: str + description: + - Rundeck User API Token. + required: true +''' diff --git a/plugins/module_utils/rundeck.py b/plugins/module_utils/rundeck.py new file mode 100644 index 0000000000..afbbb48108 --- /dev/null +++ b/plugins/module_utils/rundeck.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Phillipe Smith +# 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 + +import json + +from ansible.module_utils.urls import fetch_url, url_argument_spec +from ansible.module_utils.common.text.converters import to_native + + +def api_argument_spec(): + ''' + Creates an argument spec that can be used with any module + that will be requesting content via Rundeck API + ''' + api_argument_spec = url_argument_spec() + api_argument_spec.update(dict( + url=dict(required=True, type="str"), + api_version=dict(type="int", default=39), + api_token=dict(required=True, type="str", no_log=True) + )) + + return api_argument_spec + + +def api_request(module, endpoint, data=None, method="GET"): + """Manages Rundeck API requests via HTTP(S) + + :arg module: The AnsibleModule (used to get url, api_version, api_token, etc). + :arg endpoint: The API endpoint to be used. + :kwarg data: The data to be sent (in case of POST/PUT). + :kwarg method: "POST", "PUT", etc. + + :returns: A tuple of (**response**, **info**). Use ``response.read()`` to read the data. + The **info** contains the 'status' and other meta data. When a HttpError (status >= 400) + occurred then ``info['body']`` contains the error response data:: + + Example:: + + data={...} + resp, info = fetch_url(module, + "http://rundeck.example.org", + data=module.jsonify(data), + method="POST") + status_code = info["status"] + body = resp.read() + if status_code >= 400 : + body = info['body'] + """ + + response, info = fetch_url( + module=module, + url="%s/api/%s/%s" % ( + module.params["url"], + module.params["api_version"], + endpoint + ), + data=json.dumps(data), + method=method, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Rundeck-Auth-Token": module.params["api_token"] + } + ) + + if info["status"] == 403: + module.fail_json(msg="Token authorization failed", + execution_info=json.loads(info["body"])) + if info["status"] == 409: + module.fail_json(msg="Job executions limit reached", + execution_info=json.loads(info["body"])) + elif info["status"] >= 500: + module.fail_json(msg="Rundeck API error", + execution_info=json.loads(info["body"])) + + try: + content = response.read() + json_response = json.loads(content) + return json_response, info + except AttributeError as error: + module.fail_json(msg="Rundeck API request error", + exception=to_native(error), + execution_info=info) + except ValueError as error: + module.fail_json( + msg="No valid JSON response", + exception=to_native(error), + execution_info=content + ) diff --git a/plugins/modules/rundeck_job_executions_info.py b/plugins/modules/rundeck_job_executions_info.py new file mode 120000 index 0000000000..9c5c2138ba --- /dev/null +++ b/plugins/modules/rundeck_job_executions_info.py @@ -0,0 +1 @@ +./web_infrastructure/rundeck_job_executions_info.py \ No newline at end of file diff --git a/plugins/modules/rundeck_job_run.py b/plugins/modules/rundeck_job_run.py new file mode 120000 index 0000000000..0ac9838a56 --- /dev/null +++ b/plugins/modules/rundeck_job_run.py @@ -0,0 +1 @@ +./web_infrastructure/rundeck_job_run.py \ No newline at end of file diff --git a/plugins/modules/web_infrastructure/rundeck_job_executions_info.py b/plugins/modules/web_infrastructure/rundeck_job_executions_info.py new file mode 100644 index 0000000000..41418c66a1 --- /dev/null +++ b/plugins/modules/web_infrastructure/rundeck_job_executions_info.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Phillipe Smith +# 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 + + +DOCUMENTATION = ''' +--- +module: rundeck_job_executions_info +short_description: Query executions for a Rundeck job +description: + - This module gets the list of executions for a specified Rundeck job. +author: "Phillipe Smith (@phsmith)" +version_added: 3.8.0 +options: + job_id: + type: str + description: + - The job unique ID. + required: true + status: + type: str + description: + - The job status to filter. + choices: [succeeded, failed, aborted, running] + max: + type: int + description: + - Max results to return. + default: 20 + offset: + type: int + description: + - The start point to return the results. + default: 0 +extends_documentation_fragment: + - community.general.rundeck + - url +''' + +EXAMPLES = ''' +- name: Get Rundeck job executions info + community.general.rundeck_job_executions_info: + url: "https://rundeck.example.org" + api_version: 39 + api_token: "mytoken" + job_id: "xxxxxxxxxxxxxxxxx" + register: rundeck_job_executions_info + +- name: Show Rundeck job executions info + ansible.builtin.debug: + var: rundeck_job_executions_info.executions +''' + +RETURN = ''' +paging: + description: Results pagination info. + returned: success + type: dict + contains: + count: + description: Number of results in the response. + type: int + returned: success + total: + description: Total number of results. + type: int + returned: success + offset: + description: Offset from first of all results. + type: int + returned: success + max: + description: Maximum number of results per page. + type: int + returned: success + sample: { + "count": 20, + "total": 100, + "offset": 0, + "max": 20 + } +executions: + description: Job executions list. + returned: always + type: list + elements: dict + sample: [ + { + "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": 1633525515026, + "date": "2021-10-06T13:05:15Z" + }, + "date-ended": { + "unixtime": 1633525518386, + "date": "2021-10-06T13:05:18Z" + }, + "job": { + "id": "697af0c4-72d3-4c15-86a3-b5bfe3c6cb6a", + "averageDuration": 6381, + "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": "Plugin[com.batix.rundeck.plugins.AnsiblePlaybookInlineWorkflowStep, nodeStep: false]", + "argstring": "-exit_code 0", + "serverUUID": "5b9a1438-fa3a-457e-b254-8f3d70338068" + } + ] +''' + +# Modules import +import json + +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 RundeckJobExecutionsInfo(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.offset = self.module.params["offset"] + self.max = self.module.params["max"] + self.status = self.module.params["status"] or "" + + def job_executions(self): + response, info = api_request( + module=self.module, + endpoint="job/%s/executions?offset=%s&max=%s&status=%s" + % (quote(self.job_id), self.offset, self.max, self.status), + method="GET" + ) + + if info["status"] != 200: + self.module.fail_json( + msg=info["msg"], + executions=response + ) + + self.module.exit_json(msg="Executions info result", **response) + + +def main(): + argument_spec = api_argument_spec() + argument_spec.update(dict( + job_id=dict(required=True, type="str"), + offset=dict(type="int", default=0), + max=dict(type="int", default=20), + status=dict( + type="str", + choices=["succeeded", "failed", "aborted", "running"] + ) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + if module.params["api_version"] < 14: + module.fail_json(msg="API version should be at least 14") + + rundeck = RundeckJobExecutionsInfo(module) + rundeck.job_executions() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/web_infrastructure/rundeck_job_run.py b/plugins/modules/web_infrastructure/rundeck_job_run.py new file mode 100644 index 0000000000..1a591ad15f --- /dev/null +++ b/plugins/modules/web_infrastructure/rundeck_job_run.py @@ -0,0 +1,317 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Phillipe Smith +# 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 + + +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() diff --git a/tests/integration/targets/rundeck/aliases b/tests/integration/targets/rundeck/aliases new file mode 100644 index 0000000000..41c0922440 --- /dev/null +++ b/tests/integration/targets/rundeck/aliases @@ -0,0 +1,8 @@ +destructive +shippable/posix/group1 +skip/aix +skip/osx +skip/macos +skip/windows +skip/freebsd +unsupported diff --git a/tests/integration/targets/rundeck/defaults/main.yml b/tests/integration/targets/rundeck/defaults/main.yml new file mode 100644 index 0000000000..1fe3871186 --- /dev/null +++ b/tests/integration/targets/rundeck/defaults/main.yml @@ -0,0 +1,3 @@ +rundeck_url: http://localhost:4440 +rundeck_api_version: 39 +rundeck_job_id: 3b8a6e54-69fb-42b7-b98f-f82e59238478 diff --git a/tests/integration/targets/rundeck/files/test_job.yaml b/tests/integration/targets/rundeck/files/test_job.yaml new file mode 100644 index 0000000000..baae3ac9d8 --- /dev/null +++ b/tests/integration/targets/rundeck/files/test_job.yaml @@ -0,0 +1,23 @@ +- defaultTab: nodes + description: '' + executionEnabled: true + id: 3b8a6e54-69fb-42b7-b98f-f82e59238478 + loglevel: INFO + name: test_job + nodeFilterEditable: false + options: + - label: Exit Code + name: exit_code + value: '0' + - label: Sleep + name: sleep + value: '1' + plugins: + ExecutionLifecycle: null + scheduleEnabled: true + sequence: + commands: + - exec: sleep $RD_OPTION_SLEEP && echo "Test done!" && exit $RD_OPTION_EXIT_CODE + keepgoing: false + strategy: node-first + uuid: 3b8a6e54-69fb-42b7-b98f-f82e59238478 diff --git a/tests/integration/targets/rundeck/meta/main.yml b/tests/integration/targets/rundeck/meta/main.yml new file mode 100644 index 0000000000..3b4174fac6 --- /dev/null +++ b/tests/integration/targets/rundeck/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_rundeck diff --git a/tests/integration/targets/rundeck/tasks/main.yml b/tests/integration/targets/rundeck/tasks/main.yml new file mode 100644 index 0000000000..e9bb2beb8d --- /dev/null +++ b/tests/integration/targets/rundeck/tasks/main.yml @@ -0,0 +1,123 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + + +- name: Generate a Rundeck API Token + ansible.builtin.command: java -jar {{ rdeck_base }}/rundeck-cli.jar tokens create -u admin -d 24h -r admin + environment: + RD_URL: "{{ rundeck_url }}" + RD_USER: admin + RD_PASSWORD: admin + register: rundeck_api_token + +- name: Create a Rundeck project + community.general.rundeck_project: + name: "test_project" + api_version: "{{ rundeck_api_version }}" + url: "{{ rundeck_url }}" + token: "{{ rundeck_api_token.stdout_lines[-1] }}" + state: present + +- name: Copy test_job definition to /tmp + copy: + src: test_job.yaml + dest: /tmp/test_job.yaml + +- name: Create Rundeck job Test + ansible.builtin.command: java -jar {{ rdeck_base }}/rundeck-cli.jar jobs load -f /tmp/test_job.yaml -F yaml -p test_project + environment: + RD_URL: "{{ rundeck_url }}" + RD_USER: admin + RD_PASSWORD: admin + +- name: Wrong Rundeck API Token + community.general.rundeck_job_run: + url: "{{ rundeck_url }}" + api_version: "{{ rundeck_api_version }}" + api_token: wrong_token + job_id: "{{ rundeck_job_id }}" + ignore_errors: true + register: rundeck_job_run_wrong_token + +- name: Assert that Rundeck authorization failed + ansible.builtin.assert: + that: + - rundeck_job_run_wrong_token.msg == "Token authorization failed" + +- name: Success run Rundeck job test_job + community.general.rundeck_job_run: + url: "{{ rundeck_url }}" + api_version: "{{ rundeck_api_version }}" + api_token: "{{ rundeck_api_token.stdout_lines[-1] }}" + job_id: "{{ rundeck_job_id }}" + register: rundeck_job_run_success + +- name: Assert that Rundeck job test_job runs successfully + ansible.builtin.assert: + that: + - rundeck_job_run_success.execution_info.status == "succeeded" + +- name: Fail run Rundeck job test_job + community.general.rundeck_job_run: + url: "{{ rundeck_url }}" + api_version: "{{ rundeck_api_version }}" + api_token: "{{ rundeck_api_token.stdout_lines[-1] }}" + job_id: "{{ rundeck_job_id }}" + job_options: + exit_code: "1" + ignore_errors: true + register: rundeck_job_run_fail + +- name: Assert that Rundeck job test_job failed + ansible.builtin.assert: + that: + - rundeck_job_run_fail.execution_info.status == "failed" + +- name: Abort run Rundeck job test_job due timeout + community.general.rundeck_job_run: + url: "{{ rundeck_url }}" + api_version: "{{ rundeck_api_version }}" + api_token: "{{ rundeck_api_token.stdout_lines[-1] }}" + job_id: "{{ rundeck_job_id }}" + job_options: + sleep: "5" + wait_execution_timeout: 2 + abort_on_timeout: true + ignore_errors: true + register: rundeck_job_run_aborted + +- name: Assert that Rundeck job test_job is aborted + ansible.builtin.assert: + that: + - rundeck_job_run_aborted.execution_info.status == "aborted" + +- name: Fire-and-forget run Rundeck job test_job + community.general.rundeck_job_run: + url: "{{ rundeck_url }}" + api_version: "{{ rundeck_api_version }}" + api_token: "{{ rundeck_api_token.stdout_lines[-1] }}" + job_id: "{{ rundeck_job_id }}" + job_options: + sleep: "5" + wait_execution: False + register: rundeck_job_run_forget + +- name: Assert that Rundeck job test_job is running + ansible.builtin.assert: + that: + - rundeck_job_run_forget.execution_info.status == "running" + +- name: Get Rundeck job test_job executions info + community.general.rundeck_job_executions_info: + url: "{{ rundeck_url }}" + api_version: "{{ rundeck_api_version }}" + api_token: "{{ rundeck_api_token.stdout_lines[-1] }}" + job_id: "{{ rundeck_job_id }}" + register: rundeck_job_executions_info + +- name: Assert that Rundeck job executions info has 4 registers + ansible.builtin.assert: + that: + - rundeck_job_executions_info.paging.total | int == 4 diff --git a/tests/integration/targets/setup_rundeck/defaults/main.yml b/tests/integration/targets/setup_rundeck/defaults/main.yml new file mode 100644 index 0000000000..5b6f45f527 --- /dev/null +++ b/tests/integration/targets/setup_rundeck/defaults/main.yml @@ -0,0 +1,2 @@ +rundeck_war_url: https://packagecloud.io/pagerduty/rundeck/packages/java/org.rundeck/rundeck-3.4.4-20210920.war/artifacts/rundeck-3.4.4-20210920.war/download +rundeck_cli_url: https://github.com/rundeck/rundeck-cli/releases/download/v1.3.10/rundeck-cli-1.3.10-all.jar diff --git a/tests/integration/targets/setup_rundeck/meta/main.yml b/tests/integration/targets/setup_rundeck/meta/main.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/targets/setup_rundeck/tasks/main.yml b/tests/integration/targets/setup_rundeck/tasks/main.yml new file mode 100644 index 0000000000..41c9f1c142 --- /dev/null +++ b/tests/integration/targets/setup_rundeck/tasks/main.yml @@ -0,0 +1,37 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Skip unsupported platforms + meta: end_play + when: ansible_distribution not in ['CentOS', 'Fedora', 'Debian', 'Ubuntu'] + +- name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + when: ansible_os_family in ['Debian', 'RedHat'] + +- name: Set Rundeck base dir + set_fact: + rdeck_base: /home/rundeck + +- name: Install OpenJDK + package: + name: "{{ openjdk_pkg }}" + state: present + +- name: Install Rundeck + shell: | + mkdir -p $RDECK_BASE; + curl -k -o $RDECK_BASE/rundeck.war -L '{{ rundeck_war_url }}'; + curl -k -o $RDECK_BASE/rundeck-cli.jar -L '{{ rundeck_cli_url }}' + cd $RDECK_BASE; + java -Xmx4g -jar rundeck.war & + environment: + RDECK_BASE: "{{ rdeck_base }}" + +- name: Wait for Rundeck port 4440 + wait_for: + host: localhost + port: 4440 diff --git a/tests/integration/targets/setup_rundeck/vars/Debian.yml b/tests/integration/targets/setup_rundeck/vars/Debian.yml new file mode 100644 index 0000000000..b3bc6ac96e --- /dev/null +++ b/tests/integration/targets/setup_rundeck/vars/Debian.yml @@ -0,0 +1 @@ +openjdk_pkg: openjdk-8-jre-headless diff --git a/tests/integration/targets/setup_rundeck/vars/RedHat.yml b/tests/integration/targets/setup_rundeck/vars/RedHat.yml new file mode 100644 index 0000000000..08e32950f8 --- /dev/null +++ b/tests/integration/targets/setup_rundeck/vars/RedHat.yml @@ -0,0 +1 @@ +openjdk_pkg: java-1.8.0-openjdk