mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
* 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
(cherry picked from commit 9772485d3c
)
Co-authored-by: Phillipe Smith <phsmithcc@gmail.com>
This commit is contained in:
parent
5b9b99384f
commit
d7d1659e34
17 changed files with 841 additions and 0 deletions
4
.github/BOTMETA.yml
vendored
4
.github/BOTMETA.yml
vendored
|
@ -1160,6 +1160,10 @@ files:
|
||||||
maintainers: nerzhul
|
maintainers: nerzhul
|
||||||
$modules/web_infrastructure/rundeck_project.py:
|
$modules/web_infrastructure/rundeck_project.py:
|
||||||
maintainers: nerzhul
|
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/:
|
$modules/web_infrastructure/sophos_utm/:
|
||||||
maintainers: $team_e_spirit
|
maintainers: $team_e_spirit
|
||||||
keywords: sophos utm
|
keywords: sophos utm
|
||||||
|
|
31
plugins/doc_fragments/rundeck.py
Normal file
31
plugins/doc_fragments/rundeck.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2021, Phillipe Smith <phsmithcc@gmail.com>
|
||||||
|
# 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
|
||||||
|
'''
|
94
plugins/module_utils/rundeck.py
Normal file
94
plugins/module_utils/rundeck.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2021, Phillipe Smith <phsmithcc@gmail.com>
|
||||||
|
# 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
|
||||||
|
)
|
1
plugins/modules/rundeck_job_executions_info.py
Symbolic link
1
plugins/modules/rundeck_job_executions_info.py
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
./web_infrastructure/rundeck_job_executions_info.py
|
1
plugins/modules/rundeck_job_run.py
Symbolic link
1
plugins/modules/rundeck_job_run.py
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
./web_infrastructure/rundeck_job_run.py
|
|
@ -0,0 +1,193 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2021, Phillipe Smith <phsmithcc@gmail.com>
|
||||||
|
# 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()
|
317
plugins/modules/web_infrastructure/rundeck_job_run.py
Normal file
317
plugins/modules/web_infrastructure/rundeck_job_run.py
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2021, Phillipe Smith <phsmithcc@gmail.com>
|
||||||
|
# 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()
|
8
tests/integration/targets/rundeck/aliases
Normal file
8
tests/integration/targets/rundeck/aliases
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
destructive
|
||||||
|
shippable/posix/group1
|
||||||
|
skip/aix
|
||||||
|
skip/osx
|
||||||
|
skip/macos
|
||||||
|
skip/windows
|
||||||
|
skip/freebsd
|
||||||
|
unsupported
|
3
tests/integration/targets/rundeck/defaults/main.yml
Normal file
3
tests/integration/targets/rundeck/defaults/main.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
rundeck_url: http://localhost:4440
|
||||||
|
rundeck_api_version: 39
|
||||||
|
rundeck_job_id: 3b8a6e54-69fb-42b7-b98f-f82e59238478
|
23
tests/integration/targets/rundeck/files/test_job.yaml
Normal file
23
tests/integration/targets/rundeck/files/test_job.yaml
Normal file
|
@ -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
|
2
tests/integration/targets/rundeck/meta/main.yml
Normal file
2
tests/integration/targets/rundeck/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
dependencies:
|
||||||
|
- setup_rundeck
|
123
tests/integration/targets/rundeck/tasks/main.yml
Normal file
123
tests/integration/targets/rundeck/tasks/main.yml
Normal file
|
@ -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
|
|
@ -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
|
0
tests/integration/targets/setup_rundeck/meta/main.yml
Normal file
0
tests/integration/targets/setup_rundeck/meta/main.yml
Normal file
37
tests/integration/targets/setup_rundeck/tasks/main.yml
Normal file
37
tests/integration/targets/setup_rundeck/tasks/main.yml
Normal file
|
@ -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
|
1
tests/integration/targets/setup_rundeck/vars/Debian.yml
Normal file
1
tests/integration/targets/setup_rundeck/vars/Debian.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
openjdk_pkg: openjdk-8-jre-headless
|
1
tests/integration/targets/setup_rundeck/vars/RedHat.yml
Normal file
1
tests/integration/targets/setup_rundeck/vars/RedHat.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
openjdk_pkg: java-1.8.0-openjdk
|
Loading…
Reference in a new issue