From 7422a92836345b6ca57304e4ea781b073badaae2 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 11:30:59 -0500 Subject: [PATCH 01/10] Update service_role_config_group_info and test to align with cm_service_role_config_group_info Signed-off-by: Webster Mudge --- .../modules/service_role_config_group_info.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plugins/modules/service_role_config_group_info.py b/plugins/modules/service_role_config_group_info.py index 88f9ef65..02a1ac25 100644 --- a/plugins/modules/service_role_config_group_info.py +++ b/plugins/modules/service_role_config_group_info.py @@ -1,4 +1,5 @@ #!/usr/bin/python +#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2025 Cloudera, Inc. All Rights Reserved. @@ -37,6 +38,15 @@ required: yes aliases: - service_name + type: + description: + - The role type defining the role config group(s). + - If specified, will return all role config groups for the type. + - Mutually exclusive with O(name). + type: str + aliases: + - role_type + name: type: description: - The role type defining the role config group(s). @@ -49,6 +59,7 @@ description: - The role config group to examine. - If defined, the module will return the role config group. + - If defined, the module will return the role config group. - If the role config group does not exist, the module will return an empty result. type: str aliases: @@ -63,6 +74,13 @@ - cm-client seealso: - module: cloudera.cluster.service_role_config_group +attributes: + check_mode: + support: full +requirements: + - cm-client +seealso: + - module: cloudera.cluster.service_role_config_group """ EXAMPLES = r""" @@ -90,23 +108,28 @@ role_config_groups: description: - List of cluster service role config groups. + - List of cluster service role config groups. type: list elements: dict returned: always contains: name: + description: Name (identifier) of the role config group. description: Name (identifier) of the role config group. type: str returned: always role_type: + description: The type of the roles in this role config group. description: The type of the roles in this role config group. type: str returned: always base: + description: Flag indicating whether this is a base role config group. description: Flag indicating whether this is a base role config group. type: bool returned: always display_name: + description: A user-friendly name of the role config group, as would have been shown in the web UI. description: A user-friendly name of the role config group, as would have been shown in the web UI. type: str returned: when supported From 78b87e6f076c196b039235f3cc14f0ad429b7152 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 14:35:09 -0500 Subject: [PATCH 02/10] Update seealso docs Signed-off-by: Webster Mudge --- plugins/modules/cm_service_role_config_group.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/modules/cm_service_role_config_group.py b/plugins/modules/cm_service_role_config_group.py index e2d0bb59..76fd346e 100644 --- a/plugins/modules/cm_service_role_config_group.py +++ b/plugins/modules/cm_service_role_config_group.py @@ -45,7 +45,7 @@ description: - Whether to reset configuration parameters to only the declared entries. type: bool - default: no + default: False extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -61,6 +61,8 @@ - cm-client seealso: - module: cloudera.cluster.cm_service + - module: cloudera.cluster.cm_service_role + - module: cloudera.cluster.cm_service_role_config_group_info """ EXAMPLES = r""" From a0a3ca49c41c3002572ae09bd9d31db60674a1e6 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 14:36:12 -0500 Subject: [PATCH 03/10] Add set_role_config_group utility to update, yield, and restore Signed-off-by: Webster Mudge --- tests/unit/__init__.py | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 44b4ba0a..b8c926a5 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -37,6 +37,7 @@ MgmtRolesResourceApi, MgmtRoleCommandsResourceApi, MgmtRoleConfigGroupsResourceApi, + RoleConfigGroupsResourceApi, ServicesResourceApi, ) from cm_client.rest import ApiException @@ -431,3 +432,71 @@ def set_cm_role_config_group( rcg_api.update_role_config_group( role_config_group.name, message=f"{message}::reset", body=role_config_group ) + + +def set_role_config_group( + api_client: ApiClient, + cluster_name: str, + service_name: str, + role_config_group: ApiRoleConfigGroup, + update: ApiRoleConfigGroup, + message: str, +) -> Generator[ApiRoleConfigGroup]: + """Update a configuration for a given service role config group. + Yields the role config group and upon returning control, will reset the + configuration to its prior state. + Use with 'yield from' within a pytest fixture. + + Args: + api_client (ApiClient): CM API client + cluster_name (str): Name of the cluster + service_name (str): Name of the service + role_config_group (ApiRoleConfigGroup): The Role Config Group to manage + update (ApiRoleConfigGroup): The state to set + message (str): Transaction descriptor; will be appended with '::[re]set' + + Yields: + Generator[ApiRoleConfigGroup]: The updated Role Config Group + """ + rcg_api = RoleConfigGroupsResourceApi(api_client) + + # Ensure the modification (not a replacement) of the existing role config group + update.name = role_config_group.name + + # Update the role config group + pre_rcg = rcg_api.update_role_config_group( + cluster_name=cluster_name, + service_name=service_name, + role_config_group_name=role_config_group.name, + message=f"{message}::set", + body=update, + ) + + yield pre_rcg + + # Reread the role config group + post_rcg = rcg_api.read_role_config_group( + cluster_name=cluster_name, + service_name=service_name, + role_config_group_name=pre_rcg.name, + ) + + # Revert the changes + config_revert = resolve_parameter_updates( + {c.name: c.value for c in post_rcg.config.items}, + {c.name: c.value for c in role_config_group.config.items}, + True, + ) + + if config_revert: + role_config_group.config = ApiConfigList( + items=[ApiConfig(name=k, value=v) for k, v in config_revert.items()] + ) + + rcg_api.update_role_config_group( + cluster_name=cluster_name, + service_name=service_name, + role_config_group_name=role_config_group.name, + message=f"{message}::reset", + body=role_config_group, + ) From c0cad8b895c7f77f3e89f45b7c77257ca140e540 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 14:38:20 -0500 Subject: [PATCH 04/10] Change zk_auto to zk_function. Add zk_session for session-scoped testing. Add zk_base_role_config_group to configure and restore base role config groups for testing. Update zk_function and zk_session fixtures to use cluster hosts Signed-off-by: Webster Mudge --- tests/unit/conftest.py | 145 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e4bc0e38..b71f7935 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -41,6 +41,7 @@ ApiHostRefList, ApiRole, ApiRoleConfigGroup, + ApiRoleConfigGroupList, ApiRoleList, ApiRoleNameList, ApiRoleState, @@ -59,6 +60,7 @@ ParcelResourceApi, ParcelsResourceApi, ServicesResourceApi, + RoleConfigGroupsResourceApi, ) from cm_client.rest import ApiException, RESTClientObject @@ -69,6 +71,10 @@ Parcel, ) +from ansible_collections.cloudera.cluster.plugins.module_utils.role_config_group_utils import ( + get_base_role_config_group, +) + from ansible_collections.cloudera.cluster.plugins.module_utils.role_utils import ( get_mgmt_roles, ) @@ -79,6 +85,7 @@ provision_cm_role, set_cm_role_config, set_cm_role_config_group, + set_role_config_group, ) @@ -90,6 +97,10 @@ class ParcelNotFoundException(Exception): pass +class ZooKeeperServiceNotFoundException(Exception): + pass + + @pytest.fixture(autouse=True) def skip_python(): if sys.version_info < (3, 6): @@ -377,7 +388,7 @@ def base_cluster(cm_api_client, cms_session) -> Generator[ApiCluster]: @pytest.fixture(scope="function") -def zk_auto(cm_api_client, base_cluster, request) -> Generator[ApiService]: +def zk_function(cm_api_client, base_cluster, request) -> Generator[ApiService]: """Create a new ZooKeeper service on the provided base cluster. It starts this service, yields, and will remove this service if the tests do not. @@ -437,6 +448,66 @@ def zk_auto(cm_api_client, base_cluster, request) -> Generator[ApiService]: ) +@pytest.fixture(scope="session") +def zk_session(cm_api_client, base_cluster) -> Generator[ApiService]: + """Create a new ZooKeeper service on the provided base cluster. + It starts this service, yields, and will remove this service if the tests + do not. + + Args: + cm_api_client (ApiClient): CM API client + base_cluster (ApiCluster): Provided base cluster + + Yields: + Generator[ApiService]: ZooKeeper service + """ + + service_api = ServicesResourceApi(cm_api_client) + cm_api = ClustersResourceApi(cm_api_client) + + host = next( + (h for h in cm_api.list_hosts(cluster_name=base_cluster.name).items), None + ) + + if host is None: + raise NoHostsFoundException( + "No available hosts to assign ZooKeeper service roles" + ) + + payload = ApiService( + name="zk-session", + type="ZOOKEEPER", + roles=[ + ApiRole( + type="SERVER", + host_ref=ApiHostRef(host.host_id, host.hostname), + ), + ], + ) + + service_results = service_api.create_services( + cluster_name=base_cluster.name, body=ApiServiceList(items=[payload]) + ) + + first_run_cmd = service_api.first_run( + cluster_name=base_cluster.name, + service_name=service_results.items[0].name, + ) + + monitor_command(cm_api_client, first_run_cmd) + + zk_service = service_api.read_service( + cluster_name=base_cluster.name, service_name=service_results.items[0].name + ) + + yield zk_service + + service_api.delete_service( + cluster_name=base_cluster.name, + service_name=zk_service.name, + ) + + @pytest.fixture(scope="session") def cms(cm_api_client: ApiClient, request) -> Generator[ApiService]: """Provisions Cloudera Manager Service. If the Cloudera Manager Service @@ -850,6 +921,78 @@ def host_monitor_state( ) +@pytest.fixture(scope="function") +def zk_base_role_config_group( + cm_api_client, zk_session, request +) -> Generator[ApiRoleConfigGroup]: + """Configures the base Role Config Group for the SERVER role of a ZooKeeper service.""" + marker = request.node.get_closest_marker("role_config_group") + + if marker is None: + raise Exception("No 'role_config_group' marker found.") + + update_rcg = marker.args[0] + + rcg_api = RoleConfigGroupsResourceApi(cm_api_client) + + rcg = None + if update_rcg.name is not None: + # If it exists, update it + try: + rcg = rcg_api.read_role_config_group( + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + role_config_group_name=update_rcg.name, + ) + except ApiException as ex: + if ex.status != 404: + raise ex + + # If it doesn't exist, create, yield, and destroy + if rcg is None: + rcg = rcg_api.create_role_config_groups( + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + body=ApiRoleConfigGroupList(items=[update_rcg]), + ).items[0] + + yield rcg + + try: + rcg_api.delete_role_config_group( + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + role_config_group_name=rcg.name, + ) + except ApiException as ex: + if ex.status != 404: + raise ex + + return + else: + rcg = get_base_role_config_group( + api_client=cm_api_client, + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + role_type="SERVER", + ) + + rcg.config = rcg_api.read_config( + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + role_config_group_name=rcg.name, + ) + + yield from set_role_config_group( + api_client=cm_api_client, + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + role_config_group=rcg, + update=update_rcg, + message=f"{Path(request.node.parent.name).stem}::{request.node.name}", + ) + + def handle_commands(api_client: ApiClient, commands: ApiBulkCommandList): if commands.errors: error_msg = "\n".join(commands.errors) From 2259f8d929d0b1e0222125bca0c3673c8449d72f Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 14:38:41 -0500 Subject: [PATCH 05/10] License year Signed-off-by: Webster Mudge --- .../test_cm_service_role_config_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/cm_service_role_config_group/test_cm_service_role_config_group.py b/tests/unit/plugins/modules/cm_service_role_config_group/test_cm_service_role_config_group.py index 059524c8..43847d23 100644 --- a/tests/unit/plugins/modules/cm_service_role_config_group/test_cm_service_role_config_group.py +++ b/tests/unit/plugins/modules/cm_service_role_config_group/test_cm_service_role_config_group.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# Copyright 2025 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. From fcf507967af01d3d0cbec718ebcae03c454283f0 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 14:39:10 -0500 Subject: [PATCH 06/10] Update to use zk_session for persistent ZK service Signed-off-by: Webster Mudge --- .../test_service_role_config_group_info.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py b/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py index c23f0b63..c7382163 100644 --- a/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py +++ b/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py @@ -63,7 +63,7 @@ def test_invalid_service(conn, module_args, base_cluster): service_role_config_group_info.main() -def test_invalid_cluster(conn, module_args, base_cluster): +def test_invalid_cluster(conn, module_args, cms_session): module_args( { **conn, @@ -76,12 +76,12 @@ def test_invalid_cluster(conn, module_args, base_cluster): service_role_config_group_info.main() -def test_all_role_config_groups(conn, module_args, base_cluster, zk_auto): +def test_all_role_config_groups(conn, module_args, base_cluster, zk_session): module_args( { **conn, "cluster": base_cluster.name, - "service": zk_auto.name, + "service": zk_session.name, } ) @@ -93,12 +93,12 @@ def test_all_role_config_groups(conn, module_args, base_cluster, zk_auto): assert e.value.role_config_groups[0]["base"] == True -def test_type_role_config_group(conn, module_args, base_cluster, zk_auto): +def test_type_role_config_group(conn, module_args, base_cluster, zk_session): module_args( { **conn, "cluster": base_cluster.name, - "service": zk_auto.name, + "service": zk_session.name, "type": "SERVER", } ) @@ -112,12 +112,12 @@ def test_type_role_config_group(conn, module_args, base_cluster, zk_auto): def test_name_role_config_group( - conn, module_args, cm_api_client, base_cluster, zk_auto + conn, module_args, cm_api_client, base_cluster, zk_session ): base_rcg = get_base_role_config_group( api_client=cm_api_client, cluster_name=base_cluster.name, - service_name=zk_auto.name, + service_name=zk_session.name, role_type="SERVER", ) @@ -125,7 +125,7 @@ def test_name_role_config_group( { **conn, "cluster": base_cluster.name, - "service": zk_auto.name, + "service": zk_session.name, "name": base_rcg.name, } ) From 8970294f265876ce5f4269e6f10ed061d856b8ec Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 14:39:59 -0500 Subject: [PATCH 07/10] Update service_role_config_group module to handle configuration updates, removing role associations Signed-off-by: Webster Mudge --- plugins/modules/service_role_config_group.py | 391 +++++++------- .../test_service_role_config_group.py | 506 ++++++++++++++++++ 2 files changed, 688 insertions(+), 209 deletions(-) create mode 100644 tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py diff --git a/plugins/modules/service_role_config_group.py b/plugins/modules/service_role_config_group.py index b54ffeef..3de6dfb3 100644 --- a/plugins/modules/service_role_config_group.py +++ b/plugins/modules/service_role_config_group.py @@ -1,6 +1,7 @@ +#!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2024 Cloudera, Inc. All Rights Reserved. +# Copyright 2025 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. @@ -14,40 +15,13 @@ # 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 ( - ClouderaManagerMutableModule, -) - -from ansible_collections.cloudera.cluster.plugins.module_utils.role_config_group_utils import ( - parse_role_config_group_result, -) - -from cm_client import ( - ApiRoleConfigGroup, - ApiRoleConfigGroupList, - ApiRoleNameList, - ClustersResourceApi, - RoleConfigGroupsResourceApi, - ServicesResourceApi, -) -from cm_client.rest import ApiException - -ANSIBLE_METADATA = { - "metadata_version": "1.1", - "status": ["preview"], - "supported_by": "community", -} - DOCUMENTATION = r""" ---- module: service_role_config_group short_description: Manage a cluster service role config group. description: - Manage a cluster service role config group. author: - "Webster Mudge (@wmudge)" -requirements: - - cm-client options: cluster: description: @@ -63,57 +37,48 @@ required: True aliases: - service_name - role_config_group: + name: description: - A role config group to manage. + - If not defined, the module will target the I(base) role config group associated with the O(role_type). type: str - required: True aliases: - role_config_group_name - - name + - role_config_group role_type: description: - The role type defining the role config group. - - I(role_type) is only valid during creation. - To change the I(role_type) of an existing role config group, you must explicitly delete and recreate the role config group. type: str - required: False aliases: - type display_name: description: - The display name for this role config group in the Cloudera Manager UI. + config: + description: + - The role config group configuration to set. + - To unset a parameter, use V(None) as the value. + type: dict + aliases: + - params + - parameters purge: description: - - Flag indicating whether to reset role associations to only the declared roles. + - Whether to reset configuration parameters to only the declared entries. type: bool - required: False default: False - roles: - description: - - A list of roles associated, i.e. using, the role config group. - - If I(purge=False), any new roles will be moved to use the role config group. - - If I(purge=True), any roles not specified in the list will be reset to the C(base) role config group for the service. - type: list - elements: str - required: False - aliases: - - role_association - - role_membership - - membership state: description: - The presence or absence of the role config group. - - On I(state=absent), any associated role will be moved to the service's default group, i.e. the C(base) role config group. + - If any I(roles) are associated with role config group, you are not able to delete the group. - "NOTE: you cannot remove a C(base) role config group." type: str - required: False choices: - present - absent default: present extends_documentation_fragment: - - ansible.builtin.action_common_attributes - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint - cloudera.cluster.message @@ -124,80 +89,66 @@ support: full platform: platforms: all +requirements: + - cm-client +seealso: + - module: cloudera.cluster.cluster + - module: cloudera.cluster.service + - module: cloudera.cluster.service_role + - module: cloudera.cluster.service_role_config_group_info """ EXAMPLES = r""" ---- -- name: Create a role config group +- name: Create or update a role config group cloudera.cluster.service_role_config_group: host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" cluster: example-cluster - service: HDFS - role_config_group: Example-DATANODE - type: DATANODE + service: ZooKeeper + name: Example-ZK-Server + type: SERVER + config: + tickTime: 2500 -- name: Create or update a role config group with role associations +- name: Create or update a role config group, purging undeclared parameters cloudera.cluster.service_role_config_group: host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" cluster: example-cluster - service: HDFS - type: DATANODE - role_config_group: Example-DATANODE - roles: - - hdfs-DATANODE-a9a5b7d344404d8a304ff4b3779679a1 - -- name: Append a role association to a role config group - cloudera.cluster.cluster_service_role_config: - host: example.cloudera.com - username: "jane_smith" - password: "S&peR4Ec*re" - cluster: example-cluster - service: example-service - role_config_group: Example-DATANODE - roles: - - hdfs-DATANODE-7f3a9da5805a46e3100bae67424355ac # Now two roles - -- name: Update (purge) role associations to a role config group - cloudera.cluster.cluster_service_role_config: - host: example.cloudera.com - username: "jane_smith" - password: "S&peR4Ec*re" - cluster: example-cluster - service: example-service - role_config_group: Example-DATANODE - roles: - - hdfs-DATANODE-7f3a9da5805a46e3100bae67424355ac # Now only one role + service: ZooKeeper + name: Example-ZK-Server + type: SERVER + config: + another_parameter: 12345 purge: yes -- name: Reset all role associations to a role config group - cloudera.cluster.cluster_service_role_config: +- name: Update the base role config group for a role type + cloudera.cluster.service_role_config_group: host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" cluster: example-cluster - service: example-service - role_config_group: Example-DATANODE - roles: [] - purge: yes + service: ZooKeeper + # name: Leave blank to target the base role config group + type: SERVER + config: + tickTime: 3500 -- name: Remove a role config group +- name: Reset the configuration of a role config group cloudera.cluster.service_role_config_group: host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" cluster: example-cluster - service: HDFS - role_config_group: Example-DATANODE - state: absent -- + service: ZooKeeper + name: Example-ZK-Server + type: SERVER + purge: yes """ RETURN = r""" ---- role_config_group: description: - A service role config group. @@ -229,6 +180,15 @@ - The service name associated with this role config group. type: str returned: always + cluster_name: + description: + - The cluster name associated with the service of the role config group. + type: str + returned: always + config: + description: Set of configurations for the role config group. + type: dict + returned: when supported role_names: description: - List of role names associated with this role config group. @@ -237,6 +197,27 @@ returned: when supported """ +from cm_client import ( + ApiConfigList, + ApiRoleConfigGroup, + ApiRoleConfigGroupList, + ApiRoleNameList, + ClustersResourceApi, + RoleConfigGroupsResourceApi, + ServicesResourceApi, +) +from cm_client.rest import ApiException + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerMutableModule, + ConfigListUpdates, +) + +from ansible_collections.cloudera.cluster.plugins.module_utils.role_config_group_utils import ( + parse_role_config_group_result, + get_base_role_config_group, +) + class ClusterServiceRoleConfig(ClouderaManagerMutableModule): def __init__(self, module): @@ -245,16 +226,16 @@ def __init__(self, module): # Set the parameters self.cluster = self.get_param("cluster") self.service = self.get_param("service") - self.role_config_group = self.get_param("role_config_group") + self.name = self.get_param("name") self.role_type = self.get_param("role_type") self.display_name = self.get_param("display_name") - self.roles = self.get_param("roles") + self.config = self.get_param("config") self.purge = self.get_param("purge") self.state = self.get_param("state") # Initialize the return value self.changed = False - self.diff = {} + self.diff = dict(before=dict(), after=dict()) self.output = {} # Execute the logic @@ -262,6 +243,7 @@ def __init__(self, module): @ClouderaManagerMutableModule.handle_process def process(self): + # Confirm the presence of the cluster and service try: ClustersResourceApi(self.api_client).read_cluster(self.cluster) except ApiException as ex: @@ -280,167 +262,161 @@ def process(self): else: raise ex - api_instance = RoleConfigGroupsResourceApi(self.api_client) - existing = None - existing_roles = [] + rcg_api = RoleConfigGroupsResourceApi(self.api_client) + current = None + current_roles = [] + # Retrieve the RCG and any associated roles try: - existing = api_instance.read_role_config_group( - cluster_name=self.cluster, - role_config_group_name=self.role_config_group, - service_name=self.service, - ) - existing_roles = api_instance.read_roles( + if self.name: + current = rcg_api.read_role_config_group( + cluster_name=self.cluster, + service_name=self.service, + role_config_group_name=self.name, + ) + else: + current = get_base_role_config_group( + self.api_client, self.cluster, self.service, self.role_type + ) + + current_roles = rcg_api.read_roles( cluster_name=self.cluster, - role_config_group_name=self.role_config_group, service_name=self.service, - ) + role_config_group_name=current.name, + ).items except ApiException as ex: if ex.status != 404: raise ex if self.state == "absent": - if existing: + if current: self.changed = True + if current.base: + self.module.fail_json( + msg="Deletion failed. Role config group is a base (default) group." + ) + + if current_roles: + self.module.fail_json( + msg="Deletion failed. Role config group has existing role associations." + ) + if self.module._diff: self.diff = dict( - before=dict(roles=[r.name for r in existing_roles.items]), + before=dict( + **parse_role_config_group_result(current), + ), after={}, ) if not self.module.check_mode: - if existing_roles: - api_instance.move_roles_to_base_group( - cluster_name=self.cluster, - service_name=self.service, - body=ApiRoleNameList( - [r.name for r in existing_roles.items] - ), - ) - - api_instance.delete_role_config_group( + rcg_api.delete_role_config_group( cluster_name=self.cluster, - role_config_group_name=self.role_config_group, + role_config_group_name=current.name, service_name=self.service, ) elif self.state == "present": - if existing: - if self.role_type and self.role_type != existing.role_type: + if current: + # Check for role type changes + if self.role_type and self.role_type != current.role_type: self.module.fail_json( msg="Invalid role type. To change the role type of an existing role config group, please destroy and recreate the role config group with the designated role type." ) - if self.display_name and self.display_name != existing.display_name: + payload = ApiRoleConfigGroup( + name=current.name, role_type=current.role_type + ) + + # Check for display name changes + if self.display_name and self.display_name != current.display_name: self.changed = True if self.module._diff: - self.diff["before"].update(display_name=existing.display_name) + self.diff["before"].update(display_name=current.display_name) self.diff["after"].update(display_name=self.display_name) - if not self.module.check_mode: - api_instance.update_role_config_group( - cluster_name=self.cluster, - role_config_group_name=self.role_config_group, - service_name=self.service, - message=self.message, - body=ApiRoleConfigGroup(display_name=self.display_name), - ) + payload.display_name = self.display_name - if self.roles is not None: - existing_role_names = set([r.name for r in existing_roles.items]) - roles_add = set(self.roles) - existing_role_names + # Reconcile configurations + if self.config or self.purge: + if self.config is None: + self.config = dict() - if self.purge: - roles_del = existing_role_names - set(self.roles) - else: - roles_del = [] + updates = ConfigListUpdates(current.config, self.config, self.purge) - if self.module._diff: - self.diff["before"].update(roles=existing_role_names) - self.diff["after"].update(roles=roles_add) - - if roles_add: + if updates.changed: self.changed = True - if not self.module.check_mode: - api_instance.move_roles( - cluster_name=self.cluster, - role_config_group_name=self.role_config_group, - service_name=self.service, - body=ApiRoleNameList(list(roles_add)), - ) - - if roles_del: - self.changed = True - if not self.module.check_mode: - api_instance.move_roles_to_base_group( - cluster_name=self.cluster, - service_name=self.service, - body=ApiRoleNameList(list(roles_del)), - ) + if self.module._diff: + self.diff["before"].update(config=updates.diff["before"]) + self.diff["after"].update(config=updates.diff["after"]) + + payload.config = updates.config + + # Execute changes if needed + if self.changed and not self.module.check_mode: + current = rcg_api.update_role_config_group( + cluster_name=self.cluster, + service_name=self.service, + role_config_group_name=current.name, + message=self.message, + body=payload, + ) else: + if self.role_type is None: + self.module.fail_json( + msg="Role config group needs to be created, but is missing required arguments: role_type" + ) + self.changed = True - if self.role_type is None: - self.module.fail_json(msg="missing required arguments: role_type") + # Create the RCG + payload = ApiRoleConfigGroup( + name=self.name, + role_type=self.role_type, + ) + + if self.display_name: + payload.display_name = self.display_name + + # Set the configuration + if self.config: + payload.config = ConfigListUpdates( + ApiConfigList(items=[]), self.config, self.purge + ).config if self.module._diff: self.diff = dict( before={}, - after=dict(roles=self.roles), + after=dict( + **parse_role_config_group_result(payload), + ), ) if not self.module.check_mode: - payload = ApiRoleConfigGroup( - name=self.role_config_group, - role_type=self.role_type, - ) - - if self.display_name: - payload.display_name = self.display_name - - api_instance.create_role_config_groups( + current = rcg_api.create_role_config_groups( cluster_name=self.cluster, service_name=self.service, body=ApiRoleConfigGroupList([payload]), - ) - - if self.roles: - api_instance.move_roles( - cluster_name=self.cluster, - role_config_group_name=self.role_config_group, - service_name=self.service, - body=ApiRoleNameList(self.roles), - ) + ).items[0] + # Prepare output if self.changed: self.output = parse_role_config_group_result( - api_instance.read_role_config_group( + rcg_api.read_role_config_group( cluster_name=self.cluster, - role_config_group_name=self.role_config_group, service_name=self.service, + role_config_group_name=current.name, ) ) - - self.output.update( - role_names=[ - r.name - for r in api_instance.read_roles( - cluster_name=self.cluster, - role_config_group_name=self.role_config_group, - service_name=self.service, - ).items - ] - ) - else: - self.output = { - **parse_role_config_group_result(existing), - "role_names": [r.name for r in existing_roles.items], - } + self.output = parse_role_config_group_result(current) + self.output.update( + role_names=[r.name for r in current_roles], + ) else: self.module.fail_json(msg="Invalid state: " + self.state) @@ -450,19 +426,16 @@ def main(): argument_spec=dict( cluster=dict(required=True, aliases=["cluster_name"]), service=dict(required=True, aliases=["service_name"]), - role_config_group=dict( - required=True, aliases=["role_config_group_name", "name"] - ), + name=dict(aliases=["role_config_group_name", "role_config_group"]), display_name=dict(), role_type=dict(aliases=["type"]), - roles=dict( - type="list", - elements="str", - aliases=["role_association", "role_membership", "membership"], - ), + config=dict(type="dict", aliases=["params", "parameters"]), purge=dict(type="bool", default=False), state=dict(choices=["present", "absent"], default="present"), ), + required_one_of=[ + ["name", "role_type"], + ], supports_check_mode=True, ) diff --git a/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py b/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py new file mode 100644 index 00000000..92d004c4 --- /dev/null +++ b/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py @@ -0,0 +1,506 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2025 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 logging +import pytest + +from pathlib import Path + +from cm_client import ( + ApiConfig, + ApiConfigList, + ApiRoleConfigGroup, +) + +from ansible_collections.cloudera.cluster.plugins.modules import ( + service_role_config_group, +) + +from ansible_collections.cloudera.cluster.plugins.module_utils.role_config_group_utils import ( + get_base_role_config_group, +) + +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, + AnsibleFailJson, +) + +LOG = logging.getLogger(__name__) + + +def test_role_config_group_missing_required(conn, module_args): + module_args( + { + **conn, + "cluster": "CLUSTER", + "service": "SERVICE", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + with pytest.raises(AnsibleFailJson, match="name, role_type"): + service_role_config_group.main() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=2500, process_start_secs=25).items() + ] + ) + ) +) +def test_base_role_config_group_set( + conn, module_args, zk_base_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "type": zk_base_role_config_group.role_type, + "parameters": dict(minSessionTimeout=3000), + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(minSessionTimeout="3000", process_start_secs="25") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=2600, process_start_secs=26).items() + ] + ) + ) +) +def test_base_role_config_group_unset( + conn, module_args, zk_base_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "type": zk_base_role_config_group.role_type, + "parameters": dict(minSessionTimeout=None), + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(process_start_secs="26") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=2700, process_start_secs=27).items() + ] + ) + ) +) +def test_base_role_config_group_purge( + conn, module_args, zk_base_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "type": zk_base_role_config_group.role_type, + "parameters": dict(minSessionTimeout=2701), + "purge": True, + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(minSessionTimeout="2701") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=2800, process_start_secs=28).items() + ] + ) + ) +) +def test_base_role_config_group_purge_all( + conn, module_args, zk_base_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "type": zk_base_role_config_group.role_type, + "parameters": dict(), + "purge": True, + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict() + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +def test_base_role_config_group_absent( + conn, module_args, cm_api_client, zk_session, request +): + rcg = get_base_role_config_group( + api_client=cm_api_client, + cluster_name=zk_session.cluster_ref.cluster_name, + service_name=zk_session.name, + role_type="SERVER", + ) + + module_args( + { + **conn, + "cluster": rcg.service_ref.cluster_name, + "service": rcg.service_ref.service_name, + "type": rcg.role_type, + "state": "absent", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + with pytest.raises( + AnsibleFailJson, + match="Deletion failed\. Role config group is a base \(default\) group\.", + ) as e: + service_role_config_group.main() + + +def test_role_config_group_create(conn, module_args, zk_session, request): + module_args( + { + **conn, + "cluster": zk_session.cluster_ref.cluster_name, + "service": zk_session.name, + "type": "SERVER", + "name": f"pyest-{zk_session.name}", + "parameters": dict(minSessionTimeout=3000), + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(minSessionTimeout="3000") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Set", + role_type="SERVER", + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=2800, process_start_secs=28).items() + ] + ), + ) +) +def test_role_config_group_set(conn, module_args, zk_base_role_config_group, request): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "name": zk_base_role_config_group.name, + "parameters": dict(minSessionTimeout=3000), + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(minSessionTimeout="3000", process_start_secs="28") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Set", + role_type="SERVER", + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=2900, process_start_secs=29).items() + ] + ), + ) +) +def test_role_config_group_unset(conn, module_args, zk_base_role_config_group, request): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "name": zk_base_role_config_group.name, + "parameters": dict(minSessionTimeout=None), + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(process_start_secs="29") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Set", + role_type="SERVER", + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=3100, process_start_secs=31).items() + ] + ), + ) +) +def test_role_config_group_purge(conn, module_args, zk_base_role_config_group, request): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "name": zk_base_role_config_group.name, + "parameters": dict(minSessionTimeout=3000), + "purge": True, + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict(minSessionTimeout="3000") + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Set", + role_type="SERVER", + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=3200, process_start_secs=32).items() + ] + ), + ) +) +def test_role_config_group_purge_all( + conn, module_args, zk_base_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "name": zk_base_role_config_group.name, + "parameters": dict(), + "purge": True, + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + expected = dict() + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert expected.items() <= e.value.role_config_group["config"].items() + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert expected.items() <= e.value.role_config_group["config"].items() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Set", + role_type="SERVER", + config=ApiConfigList( + items=[ + ApiConfig(k, v) + for k, v in dict(minSessionTimeout=3100, process_start_secs=31).items() + ] + ), + ) +) +def test_role_config_group_absent( + conn, module_args, zk_base_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_base_role_config_group.service_ref.cluster_name, + "service": zk_base_role_config_group.service_ref.service_name, + "name": zk_base_role_config_group.name, + "state": "absent", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == True + assert not e.value.role_config_group + + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group.main() + + assert e.value.changed == False + assert not e.value.role_config_group From 168a15dac42ce83f249412c93b57f1d7819b6a87 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 15:59:38 -0500 Subject: [PATCH 08/10] Rename zk_base_role_config_group to the more general zk_role_config_group Signed-off-by: Webster Mudge --- tests/unit/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index b71f7935..51973852 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -922,10 +922,12 @@ def host_monitor_state( @pytest.fixture(scope="function") -def zk_base_role_config_group( +def zk_role_config_group( cm_api_client, zk_session, request ) -> Generator[ApiRoleConfigGroup]: - """Configures the base Role Config Group for the SERVER role of a ZooKeeper service.""" + """ + Creates or updates a Role Config Group of a ZooKeeper service, i.e. a SERVER role type group. + """ marker = request.node.get_closest_marker("role_config_group") if marker is None: From 69a594e72102b03f8a0a5f06f98314329503b52c Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 16:00:34 -0500 Subject: [PATCH 09/10] Add tests for invalid configurations and actions Signed-off-by: Webster Mudge --- .../test_service_role_config_group.py | 222 ++++++++++++++---- 1 file changed, 175 insertions(+), 47 deletions(-) diff --git a/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py b/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py index 92d004c4..aa07a26d 100644 --- a/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py +++ b/tests/unit/plugins/modules/service_role_config_group/test_service_role_config_group.py @@ -28,6 +28,8 @@ ApiConfig, ApiConfigList, ApiRoleConfigGroup, + ApiRoleNameList, + RoleConfigGroupsResourceApi, ) from ansible_collections.cloudera.cluster.plugins.modules import ( @@ -46,7 +48,7 @@ LOG = logging.getLogger(__name__) -def test_role_config_group_missing_required(conn, module_args): +def test_missing_required(conn, module_args): module_args( { **conn, @@ -61,6 +63,34 @@ def test_role_config_group_missing_required(conn, module_args): service_role_config_group.main() +def test_invalid_service(conn, module_args, base_cluster): + module_args( + { + **conn, + "cluster": base_cluster.name, + "service": "BOOM", + "type": "BOOM", + } + ) + + with pytest.raises(AnsibleFailJson, match="Service does not exist: BOOM"): + service_role_config_group.main() + + +def test_invalid_cluster(conn, module_args, cms_session): + module_args( + { + **conn, + "cluster": "BOOM", + "service": "BOOM", + "type": "BOOM", + } + ) + + with pytest.raises(AnsibleFailJson, match="Cluster does not exist: BOOM"): + service_role_config_group.main() + + @pytest.mark.role_config_group( ApiRoleConfigGroup( config=ApiConfigList( @@ -71,15 +101,13 @@ def test_role_config_group_missing_required(conn, module_args): ) ) ) -def test_base_role_config_group_set( - conn, module_args, zk_base_role_config_group, request -): +def test_base_role_config_group_set(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "type": zk_base_role_config_group.role_type, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "type": zk_role_config_group.role_type, "parameters": dict(minSessionTimeout=3000), "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", # _ansible_check_mode=True, @@ -113,15 +141,13 @@ def test_base_role_config_group_set( ) ) ) -def test_base_role_config_group_unset( - conn, module_args, zk_base_role_config_group, request -): +def test_base_role_config_group_unset(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "type": zk_base_role_config_group.role_type, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "type": zk_role_config_group.role_type, "parameters": dict(minSessionTimeout=None), "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", # _ansible_check_mode=True, @@ -155,15 +181,13 @@ def test_base_role_config_group_unset( ) ) ) -def test_base_role_config_group_purge( - conn, module_args, zk_base_role_config_group, request -): +def test_base_role_config_group_purge(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "type": zk_base_role_config_group.role_type, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "type": zk_role_config_group.role_type, "parameters": dict(minSessionTimeout=2701), "purge": True, "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", @@ -199,14 +223,14 @@ def test_base_role_config_group_purge( ) ) def test_base_role_config_group_purge_all( - conn, module_args, zk_base_role_config_group, request + conn, module_args, zk_role_config_group, request ): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "type": zk_base_role_config_group.role_type, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "type": zk_role_config_group.role_type, "parameters": dict(), "purge": True, "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", @@ -304,13 +328,13 @@ def test_role_config_group_create(conn, module_args, zk_session, request): ), ) ) -def test_role_config_group_set(conn, module_args, zk_base_role_config_group, request): +def test_role_config_group_set(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "name": zk_base_role_config_group.name, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": zk_role_config_group.name, "parameters": dict(minSessionTimeout=3000), "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", # _ansible_check_mode=True, @@ -346,13 +370,13 @@ def test_role_config_group_set(conn, module_args, zk_base_role_config_group, req ), ) ) -def test_role_config_group_unset(conn, module_args, zk_base_role_config_group, request): +def test_role_config_group_unset(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "name": zk_base_role_config_group.name, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": zk_role_config_group.name, "parameters": dict(minSessionTimeout=None), "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", # _ansible_check_mode=True, @@ -388,13 +412,13 @@ def test_role_config_group_unset(conn, module_args, zk_base_role_config_group, r ), ) ) -def test_role_config_group_purge(conn, module_args, zk_base_role_config_group, request): +def test_role_config_group_purge(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "name": zk_base_role_config_group.name, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": zk_role_config_group.name, "parameters": dict(minSessionTimeout=3000), "purge": True, "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", @@ -431,15 +455,13 @@ def test_role_config_group_purge(conn, module_args, zk_base_role_config_group, r ), ) ) -def test_role_config_group_purge_all( - conn, module_args, zk_base_role_config_group, request -): +def test_role_config_group_purge_all(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "name": zk_base_role_config_group.name, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": zk_role_config_group.name, "parameters": dict(), "purge": True, "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", @@ -476,15 +498,13 @@ def test_role_config_group_purge_all( ), ) ) -def test_role_config_group_absent( - conn, module_args, zk_base_role_config_group, request -): +def test_role_config_group_absent(conn, module_args, zk_role_config_group, request): module_args( { **conn, - "cluster": zk_base_role_config_group.service_ref.cluster_name, - "service": zk_base_role_config_group.service_ref.service_name, - "name": zk_base_role_config_group.name, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": zk_role_config_group.name, "state": "absent", "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", # _ansible_check_mode=True, @@ -504,3 +524,111 @@ def test_role_config_group_absent( assert e.value.changed == False assert not e.value.role_config_group + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Invalid Type", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_role_config_group_invalid_type( + conn, module_args, zk_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": "Pytest Invalid Type", + "type": "INVALID", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + with pytest.raises(AnsibleFailJson, match="Invalid role type") as e: + service_role_config_group.main() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Invalid Configuration", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_role_config_group_invalid_config( + conn, module_args, zk_role_config_group, request +): + module_args( + { + **conn, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": "Pytest Invalid Configuration", + "config": dict(invalid_configuration_parameter="BOOM"), + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + with pytest.raises(AnsibleFailJson, match="Unknown configuration attribute") as e: + service_role_config_group.main() + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Absent", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_role_config_group_existing_roles( + conn, module_args, cm_api_client, zk_role_config_group, request +): + base_rcg = get_base_role_config_group( + api_client=cm_api_client, + cluster_name=zk_role_config_group.service_ref.cluster_name, + service_name=zk_role_config_group.service_ref.service_name, + role_type="SERVER", + ) + + rcg_api = RoleConfigGroupsResourceApi(cm_api_client) + role_list = rcg_api.read_roles( + cluster_name=zk_role_config_group.service_ref.cluster_name, + service_name=zk_role_config_group.service_ref.service_name, + role_config_group_name=base_rcg.name, + ) + + rcg_api.move_roles( + cluster_name=zk_role_config_group.service_ref.cluster_name, + service_name=zk_role_config_group.service_ref.service_name, + role_config_group_name="Pytest Absent", + body=ApiRoleNameList(items=[role_list.items[0].name]), + ) + + module_args( + { + **conn, + "cluster": zk_role_config_group.service_ref.cluster_name, + "service": zk_role_config_group.service_ref.service_name, + "name": "Pytest Absent", + "state": "absent", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + # _ansible_check_mode=True, + # _ansible_diff=True, + } + ) + + with pytest.raises(AnsibleFailJson, match="existing role associations") as e: + service_role_config_group.main() + + rcg_api.move_roles_to_base_group( + cluster_name=zk_role_config_group.service_ref.cluster_name, + service_name=zk_role_config_group.service_ref.service_name, + body=ApiRoleNameList(items=[role_list.items[0].name]), + ) From 5a798ae22c499a7df373588462a4a3e34b88fd47 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Fri, 7 Mar 2025 16:01:09 -0500 Subject: [PATCH 10/10] Add multiple role config groups for better testing and filtering Signed-off-by: Webster Mudge --- .../test_service_role_config_group_info.py | 87 +++++++++++++++---- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py b/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py index c7382163..76befc6c 100644 --- a/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py +++ b/tests/unit/plugins/modules/service_role_config_group_info/test_service_role_config_group_info.py @@ -21,6 +21,11 @@ import logging import pytest +from cm_client import ( + ApiConfigList, + ApiRoleConfigGroup, +) + from ansible_collections.cloudera.cluster.plugins.modules import ( service_role_config_group_info, ) @@ -43,13 +48,19 @@ def test_missing_required(conn, module_args): def test_missing_cluster(conn, module_args): - conn.update(service="example") - module_args(conn) + module_args({**conn, "service": "example"}) with pytest.raises(AnsibleFailJson, match="cluster"): service_role_config_group_info.main() +def test_missing_service(conn, module_args, base_cluster): + module_args({**conn, "cluster": base_cluster.name}) + + with pytest.raises(AnsibleFailJson, match="service"): + service_role_config_group_info.main() + + def test_invalid_service(conn, module_args, base_cluster): module_args( { @@ -76,29 +87,41 @@ def test_invalid_cluster(conn, module_args, cms_session): service_role_config_group_info.main() -def test_all_role_config_groups(conn, module_args, base_cluster, zk_session): +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest All", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_all_role_config_groups(conn, module_args, base_cluster, zk_role_config_group): module_args( { **conn, "cluster": base_cluster.name, - "service": zk_session.name, + "service": zk_role_config_group.service_ref.service_name, } ) with pytest.raises(AnsibleExitJson) as e: service_role_config_group_info.main() - # Should be only one BASE for the SERVER - assert len(e.value.role_config_groups) == 1 - assert e.value.role_config_groups[0]["base"] == True + assert len(e.value.role_config_groups) == 2 -def test_type_role_config_group(conn, module_args, base_cluster, zk_session): +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Type", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_type_role_config_group(conn, module_args, base_cluster, zk_role_config_group): module_args( { **conn, "cluster": base_cluster.name, - "service": zk_session.name, + "service": zk_role_config_group.service_ref.service_name, "type": "SERVER", } ) @@ -106,18 +129,23 @@ def test_type_role_config_group(conn, module_args, base_cluster, zk_session): with pytest.raises(AnsibleExitJson) as e: service_role_config_group_info.main() - # Should be only one BASE for the SERVER - assert len(e.value.role_config_groups) == 1 - assert e.value.role_config_groups[0]["base"] == True + assert len(e.value.role_config_groups) == 2 -def test_name_role_config_group( - conn, module_args, cm_api_client, base_cluster, zk_session +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Base", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_name_base_role_config_group( + conn, module_args, cm_api_client, base_cluster, zk_role_config_group ): base_rcg = get_base_role_config_group( api_client=cm_api_client, cluster_name=base_cluster.name, - service_name=zk_session.name, + service_name=zk_role_config_group.service_ref.service_name, role_type="SERVER", ) @@ -125,7 +153,7 @@ def test_name_role_config_group( { **conn, "cluster": base_cluster.name, - "service": zk_session.name, + "service": zk_role_config_group.name, "name": base_rcg.name, } ) @@ -136,3 +164,30 @@ def test_name_role_config_group( # Should be only one BASE for the SERVER assert len(e.value.role_config_groups) == 1 assert e.value.role_config_groups[0]["base"] == True + + +@pytest.mark.role_config_group( + ApiRoleConfigGroup( + name="Pytest Non-Base", + role_type="SERVER", + config=ApiConfigList(items=[]), + ) +) +def test_name_base_role_config_group( + conn, module_args, base_cluster, zk_role_config_group +): + module_args( + { + **conn, + "cluster": base_cluster.name, + "service": zk_role_config_group.service_ref.service_name, + "name": "Pytest Non-Base", + } + ) + + with pytest.raises(AnsibleExitJson) as e: + service_role_config_group_info.main() + + # Should be only one non-BASE for the SERVER + assert len(e.value.role_config_groups) == 1 + assert e.value.role_config_groups[0]["base"] == False