1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Rundeck modules fixes and improvements (#6300)

* feat: add token alias to api_token parameter

* fix: return (None, info) on empty content response

* feat: update the modules for using module_utils.rundeck funcs

* docs: add changelog fragment

* fix: add trailing commas

* fix: changelog fragment invalid syntax

* fix: changelog typos

* fix: remove token aliases from api_token

* fix: add token alias to api_token param

* fix: add partial overwrite of params and docs
This commit is contained in:
Phillipe Smith 2023-04-16 08:23:39 -03:00 committed by GitHub
parent cb3ca05bd1
commit a35542d0d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 134 deletions

View file

@ -0,0 +1,8 @@
bugfixes:
- rundeck_acl_policy - fix ``TypeError - byte indices must be integers or slices, not str`` error caused by empty API response. Update the module to use ``module_utils.rundeck`` functions
(https://github.com/ansible-collections/community.general/pull/5887,
https://github.com/ansible-collections/community.general/pull/6300).
- rundeck_project - update the module to use ``module_utils.rundeck`` functions
(https://github.com/ansible-collections/community.general/issues/5742)
(https://github.com/ansible-collections/community.general/pull/6300)
- rundeck module utils - fix errors caused by the API empty responses (https://github.com/ansible-collections/community.general/pull/6300)

View file

@ -81,12 +81,18 @@ def api_request(module, endpoint, data=None, method="GET"):
try: try:
content = response.read() content = response.read()
if not content:
return None, info
else:
json_response = json.loads(content) json_response = json.loads(content)
return json_response, info return json_response, info
except AttributeError as error: except AttributeError as error:
module.fail_json(msg="Rundeck API request error", module.fail_json(
msg="Rundeck API request error",
exception=to_native(error), exception=to_native(error),
execution_info=info) execution_info=info
)
except ValueError as error: except ValueError as error:
module.fail_json( module.fail_json(
msg="No valid JSON response", msg="No valid JSON response",

View file

@ -36,22 +36,10 @@ options:
description: description:
- Sets the project name. - Sets the project name.
required: true required: true
url: api_token:
type: str
description:
- Sets the rundeck instance URL.
required: true
api_version:
type: int
description:
- Sets the API version used by module.
- API version must be at least 14.
default: 14
token:
type: str
description: description:
- Sets the token to authenticate against Rundeck API. - Sets the token to authenticate against Rundeck API.
required: true aliases: ["token"]
project: project:
type: str type: str
description: description:
@ -84,6 +72,7 @@ options:
extends_documentation_fragment: extends_documentation_fragment:
- ansible.builtin.url - ansible.builtin.url
- community.general.attributes - community.general.attributes
- community.general.rundeck
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -107,7 +96,7 @@ EXAMPLES = '''
- name: Remove a rundeck system policy - name: Remove a rundeck system policy
community.general.rundeck_acl_policy: community.general.rundeck_acl_policy:
name: "Project_02" name: "Project_01"
url: "https://rundeck.example.org" url: "https://rundeck.example.org"
token: "mytoken" token: "mytoken"
state: absent state: absent
@ -129,49 +118,25 @@ after:
''' '''
# import module snippets # import module snippets
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url, url_argument_spec
from ansible.module_utils.common.text.converters import to_text
import json
import re import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.rundeck import (
api_argument_spec,
api_request,
)
class RundeckACLManager: class RundeckACLManager:
def __init__(self, module): def __init__(self, module):
self.module = module self.module = module
def handle_http_code_if_needed(self, infos):
if infos["status"] == 403:
self.module.fail_json(msg="Token not allowed. Please ensure token is allowed or has the correct "
"permissions.", rundeck_response=infos["body"])
elif infos["status"] >= 500:
self.module.fail_json(msg="Fatal Rundeck API error.", rundeck_response=infos["body"])
def request_rundeck_api(self, query, data=None, method="GET"):
resp, info = fetch_url(self.module,
"%s/api/%d/%s" % (self.module.params["url"], self.module.params["api_version"], query),
data=json.dumps(data),
method=method,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"X-Rundeck-Auth-Token": self.module.params["token"]
})
self.handle_http_code_if_needed(info)
if resp is not None:
resp = resp.read()
if resp != b"":
try:
json_resp = json.loads(to_text(resp, errors='surrogate_or_strict'))
return json_resp, info
except ValueError as e:
self.module.fail_json(msg="Rundeck response was not a valid JSON. Exception was: %s. "
"Object was: %s" % (str(e), resp))
return resp, info
def get_acl(self): def get_acl(self):
resp, info = self.request_rundeck_api("system/acl/%s.aclpolicy" % self.module.params["name"]) resp, info = api_request(
module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"],
)
return resp return resp
def create_or_update_acl(self): def create_or_update_acl(self):
@ -181,9 +146,12 @@ class RundeckACLManager:
if self.module.check_mode: if self.module.check_mode:
self.module.exit_json(changed=True, before={}, after=self.module.params["policy"]) self.module.exit_json(changed=True, before={}, after=self.module.params["policy"])
dummy, info = self.request_rundeck_api("system/acl/%s.aclpolicy" % self.module.params["name"], resp, info = api_request(
module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"],
method="POST", method="POST",
data={"contents": self.module.params["policy"]}) data={"contents": self.module.params["policy"]},
)
if info["status"] == 201: if info["status"] == 201:
self.module.exit_json(changed=True, before={}, after=self.get_acl()) self.module.exit_json(changed=True, before={}, after=self.get_acl())
@ -202,9 +170,12 @@ class RundeckACLManager:
if self.module.check_mode: if self.module.check_mode:
self.module.exit_json(changed=True, before=facts, after=facts) self.module.exit_json(changed=True, before=facts, after=facts)
dummy, info = self.request_rundeck_api("system/acl/%s.aclpolicy" % self.module.params["name"], resp, info = api_request(
module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"],
method="PUT", method="PUT",
data={"contents": self.module.params["policy"]}) data={"contents": self.module.params["policy"]},
)
if info["status"] == 200: if info["status"] == 200:
self.module.exit_json(changed=True, before=facts, after=self.get_acl()) self.module.exit_json(changed=True, before=facts, after=self.get_acl())
@ -216,34 +187,39 @@ class RundeckACLManager:
def remove_acl(self): def remove_acl(self):
facts = self.get_acl() facts = self.get_acl()
if facts is None: if facts is None:
self.module.exit_json(changed=False, before={}, after={}) self.module.exit_json(changed=False, before={}, after={})
else: else:
# If not in check mode, remove the project # If not in check mode, remove the project
if not self.module.check_mode: if not self.module.check_mode:
self.request_rundeck_api("system/acl/%s.aclpolicy" % self.module.params["name"], method="DELETE") api_request(
module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"],
method="DELETE",
)
self.module.exit_json(changed=True, before=facts, after={}) self.module.exit_json(changed=True, before=facts, after={})
def main(): def main():
# Also allow the user to set values for fetch_url # Also allow the user to set values for fetch_url
argument_spec = url_argument_spec() argument_spec = api_argument_spec()
argument_spec.update(dict( argument_spec.update(dict(
state=dict(type='str', choices=['present', 'absent'], default='present'), state=dict(type='str', choices=['present', 'absent'], default='present'),
name=dict(required=True, type='str'), name=dict(required=True, type='str'),
url=dict(required=True, type='str'),
api_version=dict(type='int', default=14),
token=dict(required=True, type='str', no_log=True),
policy=dict(type='str'), policy=dict(type='str'),
project=dict(type='str'), project=dict(type='str'),
)) ))
argument_spec['api_token']['aliases'] = ['token']
module = AnsibleModule( module = AnsibleModule(
argument_spec=argument_spec, argument_spec=argument_spec,
required_if=[ required_if=[
['state', 'present', ['policy']], ['state', 'present', ['policy']],
], ],
supports_check_mode=True supports_check_mode=True,
) )
if not bool(re.match("[a-zA-Z0-9,.+_-]+", module.params["name"])): if not bool(re.match("[a-zA-Z0-9,.+_-]+", module.params["name"])):

View file

@ -38,22 +38,10 @@ options:
description: description:
- Sets the project name. - Sets the project name.
required: true required: true
url: api_token:
type: str
description:
- Sets the rundeck instance URL.
required: true
api_version:
type: int
description:
- Sets the API version used by module.
- API version must be at least 14.
default: 14
token:
type: str
description: description:
- Sets the token to authenticate against Rundeck API. - Sets the token to authenticate against Rundeck API.
required: true aliases: ["token"]
client_cert: client_cert:
version_added: '0.2.0' version_added: '0.2.0'
client_key: client_key:
@ -75,22 +63,25 @@ options:
extends_documentation_fragment: extends_documentation_fragment:
- ansible.builtin.url - ansible.builtin.url
- community.general.attributes - community.general.attributes
- community.general.rundeck
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Create a rundeck project - name: Create a rundeck project
community.general.rundeck_project: community.general.rundeck_project:
name: "Project_01" name: "Project_01"
api_version: 18 label: "Project 01"
description: "My Project 01"
url: "https://rundeck.example.org" url: "https://rundeck.example.org"
token: "mytoken" api_version: 39
api_token: "mytoken"
state: present state: present
- name: Remove a rundeck project - name: Remove a rundeck project
community.general.rundeck_project: community.general.rundeck_project:
name: "Project_02" name: "Project_01"
url: "https://rundeck.example.org" url: "https://rundeck.example.org"
token: "mytoken" api_token: "mytoken"
state: absent state: absent
''' '''
@ -111,60 +102,47 @@ after:
# import module snippets # import module snippets
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.rundeck import (
from ansible.module_utils.urls import fetch_url, url_argument_spec api_argument_spec,
import json api_request,
)
class RundeckProjectManager(object): class RundeckProjectManager(object):
def __init__(self, module): def __init__(self, module):
self.module = module self.module = module
def handle_http_code_if_needed(self, infos):
if infos["status"] == 403:
self.module.fail_json(msg="Token not allowed. Please ensure token is allowed or has the correct "
"permissions.", rundeck_response=infos["body"])
elif infos["status"] >= 500:
self.module.fail_json(msg="Fatal Rundeck API error.", rundeck_response=infos["body"])
def request_rundeck_api(self, query, data=None, method="GET"):
resp, info = fetch_url(self.module,
"%s/api/%d/%s" % (self.module.params["url"], self.module.params["api_version"], query),
data=json.dumps(data),
method=method,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"X-Rundeck-Auth-Token": self.module.params["token"]
})
self.handle_http_code_if_needed(info)
if resp is not None:
resp = resp.read()
if resp != "":
try:
json_resp = json.loads(resp)
return json_resp, info
except ValueError as e:
self.module.fail_json(msg="Rundeck response was not a valid JSON. Exception was: %s. "
"Object was: %s" % (to_native(e), resp))
return resp, info
def get_project_facts(self): def get_project_facts(self):
resp, info = self.request_rundeck_api("project/%s" % self.module.params["name"]) resp, info = api_request(
module=self.module,
endpoint="project/%s" % self.module.params["name"],
)
return resp return resp
def create_or_update_project(self): def create_or_update_project(self):
facts = self.get_project_facts() facts = self.get_project_facts()
if facts is None: if facts is None:
# If in check mode don't create project, simulate a fake project creation # If in check mode don't create project, simulate a fake project creation
if self.module.check_mode: if self.module.check_mode:
self.module.exit_json(changed=True, before={}, after={"name": self.module.params["name"]}) self.module.exit_json(
changed=True,
before={},
after={
"name": self.module.params["name"]
},
)
resp, info = self.request_rundeck_api("projects", method="POST", data={ resp, info = api_request(
module=self.module,
endpoint="projects",
method="POST",
data={
"name": self.module.params["name"], "name": self.module.params["name"],
"config": {} "config": {},
}) }
)
if info["status"] == 201: if info["status"] == 201:
self.module.exit_json(changed=True, before={}, after=self.get_project_facts()) self.module.exit_json(changed=True, before={}, after=self.get_project_facts())
@ -181,21 +159,25 @@ class RundeckProjectManager(object):
else: else:
# If not in check mode, remove the project # If not in check mode, remove the project
if not self.module.check_mode: if not self.module.check_mode:
self.request_rundeck_api("project/%s" % self.module.params["name"], method="DELETE") api_request(
module=self.module,
endpoint="project/%s" % self.module.params["name"],
method="DELETE",
)
self.module.exit_json(changed=True, before=facts, after={}) self.module.exit_json(changed=True, before=facts, after={})
def main(): def main():
# Also allow the user to set values for fetch_url # Also allow the user to set values for fetch_url
argument_spec = url_argument_spec() argument_spec = api_argument_spec()
argument_spec.update(dict( argument_spec.update(dict(
state=dict(type='str', choices=['present', 'absent'], default='present'), state=dict(type='str', choices=['present', 'absent'], default='present'),
name=dict(required=True, type='str'), name=dict(required=True, type='str'),
url=dict(required=True, type='str'),
api_version=dict(type='int', default=14),
token=dict(required=True, type='str', no_log=True),
)) ))
argument_spec['api_token']['aliases'] = ['token']
module = AnsibleModule( module = AnsibleModule(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=True supports_check_mode=True