From 2c636ff21a91a8ed2ca28c895fd6f83f5a9f80f3 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Thu, 21 Nov 2024 18:32:18 +0000 Subject: [PATCH 1/6] Add User Module Signed-off-by: rsuplina --- plugins/modules/user.py | 295 ++++++++++++++++++ plugins/modules/user_info.py | 137 ++++++++ tests/unit/plugins/modules/user/test_user.py | 83 +++++ .../modules/user_info/test_user_info.py | 71 +++++ 4 files changed, 586 insertions(+) create mode 100644 plugins/modules/user.py create mode 100644 plugins/modules/user_info.py create mode 100644 tests/unit/plugins/modules/user/test_user.py create mode 100644 tests/unit/plugins/modules/user_info/test_user_info.py diff --git a/plugins/modules/user.py b/plugins/modules/user.py new file mode 100644 index 00000000..7845631b --- /dev/null +++ b/plugins/modules/user.py @@ -0,0 +1,295 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) +from cm_client.rest import ApiException +from cm_client import ( + UsersResourceApi, + ApiUser2, + ApiAuthRoleRef, + ApiUser2List, + AuthRolesResourceApi, +) + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: user +short_description: Create, delete or update users within Cloudera Manager +description: + - Creates a user with specified authorization roles in Cloudera Manager, or updates roles for an existing user. + - Supports purging roles or adding new roles to the existing list. + - Enables the deletion of a user along with its associated roles if desired. +author: + - "Ronald Suplina (@rsuplina)" +requirements: + - cm_client +options: + account_name: + description: + - The name of the user account to be managed. + type: str + required: true + account_password: + description: + - The password for the account. + - Required when creating a new account. + type: str + required: false + roles: + description: + - A list of authentication roles associated with the account. + - Existing roles are preserved unless C(purge) is set to True. + type: list + required: false + aliases: + - auth_roles + purge: + description: + - When set to True, ensures that roles not listed in C(roles) are removed from the account. + type: bool + default: false + state: + description: + - Controls the desired state of the account. + - C(present) ensures the account exists with the specified parameters. + - C(absent) deletes the account and its associated roles. + type: str + default: present + choices: + - present + - absent +""" + +EXAMPLES = r""" +--- +- name: Create new Administrator user + cloudera.cluster.user: + host: example.cloudera.com + port: "7180" + username: "jane_smith" + password: "S&peR4Ec*re" + account_name: "admin_user" + account_password: "Password123" + roles: ["Full Administrator"] + state: "Present" + purge: false + +- name: Add additional roles to user + cloudera.cluster.user: + host: example.cloudera.com + port: "7180" + username: "jane_smith" + password: "S&peR4Ec*re" + account_name: "john" + account_password: "Password123" + roles: ["Configurator","Dashboard User","Limited Operator"] + state: "Present" + +- name: Reduce permissions on user to a single role + cloudera.cluster.user: + host: example.cloudera.com + port: "7180" + username: "jane_smith" + password: "S&peR4Ec*re" + account_name: "john" + account_password: "Password123" + roles: ["Dashboard User"] + state: "Present" + purge: true + +- name: Remove specified user + cloudera.cluster.user: + host: example.cloudera.com + port: "7180" + username: "jane_smith" + password: "S&peR4Ec*re" + account_name: "john" + roles: ["Dashboard User"] + state: "absent" + +""" + +RETURN = r""" +--- +user: + description: List of users within the cluster + type: dict + elements: dict + returned: always + contains: + name: + description: The username, which is unique within a Cloudera Manager installation. + type: str + returned: always + auth_roles: + description: A list of ApiAuthRole objects representing the authentication roles assigned to the user. + type: list + returned: optional +""" + + +class ClouderaUserInfo(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaUserInfo, self).__init__(module) + + # Set the parameters + self.account_name = self.get_param("account_name") + self.account_password = self.get_param("account_password") + self.roles = self.get_param("roles") + self.state = self.get_param("state") + self.purge = self.get_param("purge") + + # Initialize the return values + self.user_output = [] + self.changed = False + self.diff = {} + + # Execute the logic + self.process() + + @ClouderaManagerModule.handle_process + def process(self): + api_instance = UsersResourceApi(self.api_client) + auth_role_api_instance = AuthRolesResourceApi(self.api_client) + all_roles = auth_role_api_instance.read_auth_roles().to_dict()["items"] + existing = [] + + try: + existing = api_instance.read_user2(self.account_name).to_dict() + except ApiException as ex: + if ex.status == 404: + pass + else: + raise ex + + if self.state == "present": + + if existing: + incoming_roles = [ + role["uuid"] + for role in all_roles + if role["display_name"] in self.roles + ] + existing_roles = [role["uuid"] for role in existing["auth_roles"] or []] + + if self.module._diff: + current_roles = set(existing_roles) + incoming_roles_set = set(incoming_roles) + self.diff.update( + before=list(current_roles - incoming_roles_set), + after=list(incoming_roles_set - current_roles), + ) + if self.purge: + roles_to_add = incoming_roles + else: + roles_to_add = list(set(existing_roles) | set(incoming_roles)) + + + if roles_to_add: + auth_roles = [ + ApiAuthRoleRef(uuid=role_uuid) for role_uuid in roles_to_add + ] + self.user_output = api_instance.update_user2( + self.account_name, + body=ApiUser2( + name=self.account_name, + auth_roles=auth_roles, + password=self.account_password, + ), + ).to_dict() + self.changed = True + else: + auth_roles = [] + self.user_output = api_instance.update_user2( + self.account_name, + body=ApiUser2( + name=self.account_name, + auth_roles=auth_roles, + password=self.account_password, + ), + ).to_dict() + self.changed = True + + else: + incoming_roles = [ + role["uuid"] + for role in all_roles + if role["display_name"] in self.roles + ] + auth_roles = [ + ApiAuthRoleRef(uuid=role_uuid) for role_uuid in incoming_roles + ] + api_instance.create_users2( + body=ApiUser2List( + items=[ + ApiUser2( + name=self.account_name, + auth_roles=auth_roles, + password=self.account_password, + ) + ] + ) + ) + self.user_output = api_instance.read_user2(self.account_name).to_dict() + + self.changed = True + + if self.state == "absent": + if existing: + self.user_output = api_instance.delete_user2(self.account_name).to_dict() + self.changed = True + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + account_name=dict(required=True, type="str"), + account_password=dict(required=False, type="str"), + roles=dict(required=False, type="list", aliases=["auth_roles"]), + purge=dict(type="bool", default=False), + state=dict( + type="str", + default="present", + choices=["present", "absent"], + ), + ), + supports_check_mode=False, + ) + + result = ClouderaUserInfo(module) + + output = dict( + changed=False, + user_output=result.user_output, + ) + if module._diff: + output.update(diff=result.diff) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/user_info.py b/plugins/modules/user_info.py new file mode 100644 index 00000000..f71dc4f5 --- /dev/null +++ b/plugins/modules/user_info.py @@ -0,0 +1,137 @@ +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) +from cm_client.rest import ApiException +from cm_client import UsersResourceApi + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: user_info +short_description: Retrieve user details and associated authentication roles. +description: + - Provides details for a specific user or retrieves all users configured in Cloudera Manager. + - Includes information about authentication roles associated with each user. +author: + - "Ronald Suplina (@rsuplina)" +requirements: + - cm_client +options: + account_name: + description: + - The name of the user account to be managed. + type: str + required: false +""" + +EXAMPLES = r""" +--- +- name: Get list of all users in Cloudera Manager + cloudera.cluster.user_info: + host: example.cloudera.com + port: "7180" + username: "jane_smith" + password: "S&peR4Ec*re" + +- name: Get details for specific user + cloudera.cluster.user_info: + host: example.cloudera.com + port: "7180" + username: "jane_smith" + password: "S&peR4Ec*re" + account_name: "john" + +""" + +RETURN = r""" +--- +user_info: + description: + - Retrieve details of single user or all users within the Cloudera Manager + type: list + elements: dict + returned: always + contains: + name: + description: The username, which is unique within a Cloudera Manager installation. + type: str + returned: always + auth_roles: + description: A list of Authorization Role objects representing the authentication roles assigned to the user. + type: list + returned: optional +""" + + +class ClouderaUserInfo(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaUserInfo, self).__init__(module) + + # Initialize the return values + self.account_name = self.get_param("account_name") + self.changed = False + + # Execute the logic + self.process() + + @ClouderaManagerModule.handle_process + def process(self): + api_instance = UsersResourceApi(self.api_client) + + try: + if self.account_name: + self.user_info_output = [ + api_instance.read_user2(self.account_name).to_dict() + ] + else: + self.user_info_output = api_instance.read_users2().to_dict()["items"] + + except ApiException as e: + if e.status == 404: + self.user_info = f"User {self.account_name} does not exist." + self.module.fail_json(msg=str(self.user_info_output)) + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + account_name=dict(required=False, type="str"), + ), + supports_check_mode=False, + ) + + result = ClouderaUserInfo(module) + + output = dict( + changed=False, + user_info_output=result.user_info_output, + ) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/user/test_user.py b/tests/unit/plugins/modules/user/test_user.py new file mode 100644 index 00000000..1b1c3608 --- /dev/null +++ b/tests/unit/plugins/modules/user/test_user.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +import os +import logging +import pytest + +from ansible_collections.cloudera.cluster.plugins.modules import user +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, + AnsibleFailJson, +) + +LOG = logging.getLogger(__name__) + + +@pytest.fixture +def conn(): + conn = dict(username=os.getenv("CM_USERNAME"), password=os.getenv("CM_PASSWORD")) + + if os.getenv("CM_HOST", None): + conn.update(host=os.getenv("CM_HOST")) + + if os.getenv("CM_PORT", None): + conn.update(port=os.getenv("CM_PORT")) + + if os.getenv("CM_PROXY", None): + conn.update(proxy=os.getenv("CM_PROXY")) + + return { + **conn, + "verify_tls": "no", + "debug": "no", + } + +def test_pytest_create_new_user(module_args, conn): + conn.update( + account_name = 'John', + account_password = 'passowrd', + roles=['Configurator','Dashboard User','Limited Operator'], + state = "present", + purge = True, + + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + user.main() + + LOG.info(str(e.value.user_output)) + +def test_pytest_remove_user(module_args, conn): + conn.update( + account_name = 'John', + state = "absent", + + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + user.main() + + LOG.info(str(e.value.user_output)) + + diff --git a/tests/unit/plugins/modules/user_info/test_user_info.py b/tests/unit/plugins/modules/user_info/test_user_info.py new file mode 100644 index 00000000..1cc621c4 --- /dev/null +++ b/tests/unit/plugins/modules/user_info/test_user_info.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +import os +import logging +import pytest + +from ansible_collections.cloudera.cluster.plugins.modules import user_info +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, + AnsibleFailJson, +) + +LOG = logging.getLogger(__name__) + + +@pytest.fixture +def conn(): + conn = dict(username=os.getenv("CM_USERNAME"), password=os.getenv("CM_PASSWORD")) + + if os.getenv("CM_HOST", None): + conn.update(host=os.getenv("CM_HOST")) + + if os.getenv("CM_PORT", None): + conn.update(port=os.getenv("CM_PORT")) + + if os.getenv("CM_PROXY", None): + conn.update(proxy=os.getenv("CM_PROXY")) + + return { + **conn, + "verify_tls": "no", + "debug": "no", + } + +def test_pytest_all_userss(module_args, conn): + conn.update() + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + user_info.main() + + LOG.info(str(e.value.user_info)) + +def test_user_get_single_user(module_args, conn): + conn.update( + account_name="admin18", + ) + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + user_info.main() + + LOG.info(str(e.value.user_info_output)) + From fb93cff2c6dfcc5cc67c716271eeeff2f432628e Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 22 Nov 2024 10:25:40 +0000 Subject: [PATCH 2/6] Edit linting issues Signed-off-by: rsuplina --- plugins/modules/user.py | 7 +++-- plugins/modules/user_info.py | 2 +- tests/unit/plugins/modules/user/test_user.py | 33 ++++++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/plugins/modules/user.py b/plugins/modules/user.py index 7845631b..95cfcc39 100644 --- a/plugins/modules/user.py +++ b/plugins/modules/user.py @@ -93,7 +93,7 @@ state: "Present" purge: false -- name: Add additional roles to user +- name: Add additional roles to user cloudera.cluster.user: host: example.cloudera.com port: "7180" @@ -203,7 +203,6 @@ def process(self): else: roles_to_add = list(set(existing_roles) | set(incoming_roles)) - if roles_to_add: auth_roles = [ ApiAuthRoleRef(uuid=role_uuid) for role_uuid in roles_to_add @@ -255,7 +254,9 @@ def process(self): if self.state == "absent": if existing: - self.user_output = api_instance.delete_user2(self.account_name).to_dict() + self.user_output = api_instance.delete_user2( + self.account_name + ).to_dict() self.changed = True diff --git a/plugins/modules/user_info.py b/plugins/modules/user_info.py index f71dc4f5..f740e51a 100644 --- a/plugins/modules/user_info.py +++ b/plugins/modules/user_info.py @@ -65,7 +65,7 @@ RETURN = r""" --- user_info: - description: + description: - Retrieve details of single user or all users within the Cloudera Manager type: list elements: dict diff --git a/tests/unit/plugins/modules/user/test_user.py b/tests/unit/plugins/modules/user/test_user.py index 1b1c3608..f34e039c 100644 --- a/tests/unit/plugins/modules/user/test_user.py +++ b/tests/unit/plugins/modules/user/test_user.py @@ -49,14 +49,14 @@ def conn(): "debug": "no", } + def test_pytest_create_new_user(module_args, conn): conn.update( - account_name = 'John', - account_password = 'passowrd', - roles=['Configurator','Dashboard User','Limited Operator'], - state = "present", - purge = True, - + account_name="John", + account_password="passowrd", + roles=["Configurator", "Dashboard User", "Limited Operator"], + state="present", + purge=True, ) module_args(conn) @@ -66,11 +66,12 @@ def test_pytest_create_new_user(module_args, conn): LOG.info(str(e.value.user_output)) -def test_pytest_remove_user(module_args, conn): - conn.update( - account_name = 'John', - state = "absent", +def test_pytest_create_new_admin_user(module_args, conn): + conn.update( + account_name="Admin2", + account_password="passowrd", + roles=["Full Administrator"], ) module_args(conn) @@ -81,3 +82,15 @@ def test_pytest_remove_user(module_args, conn): LOG.info(str(e.value.user_output)) +def test_pytest_remove_user(module_args, conn): + conn.update( + account_name="John", + state="absent", + ) + + module_args(conn) + + with pytest.raises(AnsibleExitJson) as e: + user.main() + + LOG.info(str(e.value.user_output)) From a8f054ee3926eadae07ee66c80919688dee2be41 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 22 Nov 2024 10:27:53 +0000 Subject: [PATCH 3/6] edit space linting Signed-off-by: rsuplina --- tests/unit/plugins/modules/user_info/test_user_info.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/plugins/modules/user_info/test_user_info.py b/tests/unit/plugins/modules/user_info/test_user_info.py index 1cc621c4..e9b4cf88 100644 --- a/tests/unit/plugins/modules/user_info/test_user_info.py +++ b/tests/unit/plugins/modules/user_info/test_user_info.py @@ -49,6 +49,7 @@ def conn(): "debug": "no", } + def test_pytest_all_userss(module_args, conn): conn.update() module_args(conn) @@ -58,6 +59,7 @@ def test_pytest_all_userss(module_args, conn): LOG.info(str(e.value.user_info)) + def test_user_get_single_user(module_args, conn): conn.update( account_name="admin18", @@ -67,5 +69,4 @@ def test_user_get_single_user(module_args, conn): with pytest.raises(AnsibleExitJson) as e: user_info.main() - LOG.info(str(e.value.user_info_output)) - + LOG.info(str(e.value.user_info_output)) \ No newline at end of file From 5ee2321dcf3b1cf4f134044d7ce79d55a63af158 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 22 Nov 2024 10:29:00 +0000 Subject: [PATCH 4/6] add new line at end Signed-off-by: rsuplina --- tests/unit/plugins/modules/user_info/test_user_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/user_info/test_user_info.py b/tests/unit/plugins/modules/user_info/test_user_info.py index e9b4cf88..ef28a35e 100644 --- a/tests/unit/plugins/modules/user_info/test_user_info.py +++ b/tests/unit/plugins/modules/user_info/test_user_info.py @@ -69,4 +69,4 @@ def test_user_get_single_user(module_args, conn): with pytest.raises(AnsibleExitJson) as e: user_info.main() - LOG.info(str(e.value.user_info_output)) \ No newline at end of file + LOG.info(str(e.value.user_info_output)) From 1b90e2d73b949982bcb5579f6a5f9650c6965da0 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 22 Nov 2024 14:31:51 +0000 Subject: [PATCH 5/6] Add requested changes Signed-off-by: rsuplina --- plugins/modules/user.py | 4 ++-- plugins/modules/user_info.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/modules/user.py b/plugins/modules/user.py index 95cfcc39..c3e31a8a 100644 --- a/plugins/modules/user.py +++ b/plugins/modules/user.py @@ -131,7 +131,7 @@ RETURN = r""" --- user: - description: List of users within the cluster + description: Details of a single user within the cluster type: dict elements: dict returned: always @@ -141,7 +141,7 @@ type: str returned: always auth_roles: - description: A list of ApiAuthRole objects representing the authentication roles assigned to the user. + description: Cloudera Manager authorization roles assigned to the user. type: list returned: optional """ diff --git a/plugins/modules/user_info.py b/plugins/modules/user_info.py index f740e51a..b831d81c 100644 --- a/plugins/modules/user_info.py +++ b/plugins/modules/user_info.py @@ -64,7 +64,7 @@ RETURN = r""" --- -user_info: +users: description: - Retrieve details of single user or all users within the Cloudera Manager type: list @@ -76,7 +76,7 @@ type: str returned: always auth_roles: - description: A list of Authorization Role objects representing the authentication roles assigned to the user. + description: Cloudera Manager authorization roles assigned to the user. type: list returned: optional """ @@ -86,8 +86,12 @@ class ClouderaUserInfo(ClouderaManagerModule): def __init__(self, module): super(ClouderaUserInfo, self).__init__(module) - # Initialize the return values + # Set the parameters + self.account_name = self.get_param("account_name") + + # Initialize the return values + self.user_info_output = [] self.changed = False # Execute the logic @@ -107,8 +111,9 @@ def process(self): except ApiException as e: if e.status == 404: - self.user_info = f"User {self.account_name} does not exist." - self.module.fail_json(msg=str(self.user_info_output)) + pass + else: + raise e def main(): From 54dcbb2668e304ee3f4c02cdab225520db8edd04 Mon Sep 17 00:00:00 2001 From: rsuplina Date: Fri, 22 Nov 2024 14:33:39 +0000 Subject: [PATCH 6/6] present lowercase Signed-off-by: rsuplina --- plugins/modules/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/user.py b/plugins/modules/user.py index c3e31a8a..dcb53c16 100644 --- a/plugins/modules/user.py +++ b/plugins/modules/user.py @@ -90,7 +90,7 @@ account_name: "admin_user" account_password: "Password123" roles: ["Full Administrator"] - state: "Present" + state: "present" purge: false - name: Add additional roles to user @@ -102,7 +102,7 @@ account_name: "john" account_password: "Password123" roles: ["Configurator","Dashboard User","Limited Operator"] - state: "Present" + state: "present" - name: Reduce permissions on user to a single role cloudera.cluster.user: @@ -113,7 +113,7 @@ account_name: "john" account_password: "Password123" roles: ["Dashboard User"] - state: "Present" + state: "present" purge: true - name: Remove specified user