Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rundeck modules fixes and improvements #6300

Merged
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 11 additions & 5 deletions plugins/module_utils/rundeck.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,18 @@ def api_request(module, endpoint, data=None, method="GET"):

try:
content = response.read()
json_response = json.loads(content)
return json_response, info

if not content:
return None, info
else:
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)
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",
Expand Down
103 changes: 40 additions & 63 deletions plugins/modules/rundeck_acl_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,12 @@
description:
- Sets the project name.
required: true
url:
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:
api_token:
type: str
description:
- Sets the token to authenticate against Rundeck API.
required: true
aliases: ["token"]
project:
type: str
description:
Expand Down Expand Up @@ -82,8 +72,9 @@
validate_certs:
version_added: '0.2.0'
phsmith marked this conversation as resolved.
Show resolved Hide resolved
extends_documentation_fragment:
- ansible.builtin.url
- community.general.attributes
- ansible.builtin.url
- community.general.attributes
- community.general.rundeck
'''

EXAMPLES = '''
Expand All @@ -107,7 +98,7 @@

- name: Remove a rundeck system policy
community.general.rundeck_acl_policy:
name: "Project_02"
name: "Project_01"
url: "https://rundeck.example.org"
token: "mytoken"
state: absent
Expand All @@ -129,49 +120,25 @@
'''

# 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

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.rundeck import (
api_argument_spec,
api_request,
)


class RundeckACLManager:
def __init__(self, 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):
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

def create_or_update_acl(self):
Expand All @@ -181,9 +148,12 @@ def create_or_update_acl(self):
if self.module.check_mode:
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"],
method="POST",
data={"contents": self.module.params["policy"]})
resp, info = api_request(
module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"],
method="POST",
data={"contents": self.module.params["policy"]},
)

if info["status"] == 201:
self.module.exit_json(changed=True, before={}, after=self.get_acl())
Expand All @@ -202,9 +172,12 @@ def create_or_update_acl(self):
if self.module.check_mode:
self.module.exit_json(changed=True, before=facts, after=facts)

dummy, info = self.request_rundeck_api("system/acl/%s.aclpolicy" % self.module.params["name"],
method="PUT",
data={"contents": self.module.params["policy"]})
resp, info = api_request(
module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"],
method="PUT",
data={"contents": self.module.params["policy"]},
)

if info["status"] == 200:
self.module.exit_json(changed=True, before=facts, after=self.get_acl())
Expand All @@ -216,24 +189,28 @@ def create_or_update_acl(self):

def remove_acl(self):
facts = self.get_acl()

if facts is None:
self.module.exit_json(changed=False, before={}, after={})
else:
# If not in check mode, remove the project
if not self.module.check_mode:
self.request_rundeck_api("system/acl/%s.aclpolicy" % self.module.params["name"], method="DELETE")
self.module.exit_json(changed=True, before=facts, after={})
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={})


def main():
# Also allow the user to set values for fetch_url
argument_spec = url_argument_spec()
argument_spec = api_argument_spec()
argument_spec.update(dict(
state=dict(type='str', choices=['present', 'absent'], default='present'),
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),
api_token=dict(required=True, type="str", no_log=True, aliases=["token"]),
phsmith marked this conversation as resolved.
Show resolved Hide resolved
policy=dict(type='str'),
project=dict(type='str'),
))
Expand All @@ -243,7 +220,7 @@ def main():
required_if=[
['state', 'present', ['policy']],
],
supports_check_mode=True
supports_check_mode=True,
)

if not bool(re.match("[a-zA-Z0-9,.+_-]+", module.params["name"])):
Expand Down
109 changes: 46 additions & 63 deletions plugins/modules/rundeck_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,12 @@
description:
- Sets the project name.
required: true
url:
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:
api_token:
type: str
description:
- Sets the token to authenticate against Rundeck API.
required: true
aliases: ["token"]
client_cert:
version_added: '0.2.0'
client_key:
Expand All @@ -73,24 +63,27 @@
validate_certs:
version_added: '0.2.0'
extends_documentation_fragment:
- ansible.builtin.url
- community.general.attributes
- ansible.builtin.url
- community.general.attributes
- community.general.rundeck
'''

EXAMPLES = '''
- name: Create a rundeck project
community.general.rundeck_project:
name: "Project_01"
api_version: 18
label: "Project 01"
description: "My Project 01"
url: "https://rundeck.example.org"
token: "mytoken"
api_version: 39
api_token: "mytoken"
state: present

- name: Remove a rundeck project
community.general.rundeck_project:
name: "Project_02"
name: "Project_01"
url: "https://rundeck.example.org"
token: "mytoken"
api_token: "mytoken"
state: absent
'''

Expand All @@ -111,60 +104,47 @@

# import module snippets
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.urls import fetch_url, url_argument_spec
import json
from ansible_collections.community.general.plugins.module_utils.rundeck import (
api_argument_spec,
api_request,
)


class RundeckProjectManager(object):
def __init__(self, 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):
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

def create_or_update_project(self):
facts = self.get_project_facts()

if facts is None:
# If in check mode don't create project, simulate a fake project creation
if self.module.check_mode:
self.module.exit_json(changed=True, before={}, after={"name": self.module.params["name"]})

resp, info = self.request_rundeck_api("projects", method="POST", data={
"name": self.module.params["name"],
"config": {}
})
self.module.exit_json(
changed=True,
before={},
after={
"name": self.module.params["name"]
},
)

resp, info = api_request(
module=self.module,
endpoint="projects",
method="POST",
data={
"name": self.module.params["name"],
"config": {},
}
)

if info["status"] == 201:
self.module.exit_json(changed=True, before={}, after=self.get_project_facts())
Expand All @@ -181,19 +161,22 @@ def remove_project(self):
else:
# If not in check mode, remove the project
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={})


def main():
# Also allow the user to set values for fetch_url
argument_spec = url_argument_spec()
argument_spec = api_argument_spec()
argument_spec.update(dict(
state=dict(type='str', choices=['present', 'absent'], default='present'),
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),
api_token=dict(required=True, type="str", no_log=True, aliases=["token"]),
))

module = AnsibleModule(
Expand Down