From 129af5073504e2c14df3894245e1fec48297c9c2 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Tue, 29 Jul 2025 16:16:15 -0400 Subject: [PATCH 1/9] Add 'view' parameter for returned values Signed-off-by: Webster Mudge --- plugins/module_utils/cm_utils.py | 2 +- plugins/module_utils/host_utils.py | 7 +- plugins/module_utils/role_utils.py | 24 +-- plugins/module_utils/service_utils.py | 43 ++++- plugins/modules/cluster.py | 12 ++ plugins/modules/cluster_info.py | 2 +- plugins/modules/cm_config.py | 32 ++-- plugins/modules/cm_service.py | 21 ++- plugins/modules/cm_service_config.py | 9 +- plugins/modules/cm_service_info.py | 26 ++- plugins/modules/cm_service_role.py | 25 ++- plugins/modules/cm_service_role_info.py | 20 ++- plugins/modules/external_account.py | 164 ++++++++++-------- plugins/modules/external_account_info.py | 79 ++++----- plugins/modules/external_user_mappings.py | 16 +- .../modules/external_user_mappings_info.py | 28 ++- plugins/modules/host.py | 15 +- plugins/modules/host_info.py | 24 ++- plugins/modules/role.py | 28 ++- plugins/modules/role_info.py | 1 + plugins/modules/service.py | 26 +++ plugins/modules/service_info.py | 24 +++ 22 files changed, 453 insertions(+), 175 deletions(-) diff --git a/plugins/module_utils/cm_utils.py b/plugins/module_utils/cm_utils.py index 495899b6..3e2a0a6c 100644 --- a/plugins/module_utils/cm_utils.py +++ b/plugins/module_utils/cm_utils.py @@ -623,7 +623,7 @@ def call_api(self, path, method, query=None, field="items", body=None): data = data[field] return data if isinstance(data, list) else [data] - def get_cm_config(self, scope: str = "summary") -> ApiConfigList: + def get_cm_config(self, scope: str = "summary") -> list[ApiConfig]: return ClouderaManagerResourceApi(self.api_client).get_config(view=scope).items def wait_command(self, command: ApiCommand, polling: int = 10, delay: int = 5): diff --git a/plugins/module_utils/host_utils.py b/plugins/module_utils/host_utils.py index 72793996..d77f30a6 100644 --- a/plugins/module_utils/host_utils.py +++ b/plugins/module_utils/host_utils.py @@ -132,7 +132,7 @@ def get_host( api_client: ApiClient, hostname: str = None, host_id: str = None, - view: str = "summary", + view: str = "full", ) -> ApiHost: """Retrieve a Host by either hostname or host ID. @@ -172,6 +172,7 @@ def get_host_ref( api_client: ApiClient, hostname: str = None, host_id: str = None, + view: str = "full", ) -> ApiHostRef: """Retrieve a Host Reference by either hostname or host ID. @@ -186,7 +187,7 @@ def get_host_ref( Returns: ApiHostRef: Host reference object. If not found, returns None. """ - host = get_host(api_client, hostname, host_id) + host = get_host(api_client, hostname, host_id, view) if host is not None: return ApiHostRef(host.host_id, host.hostname) @@ -249,6 +250,7 @@ def reconcile_host_role_configs( check_mode: bool, skip_redacted: bool, message: str = None, + view: str = "full", ) -> tuple[list[dict], list[dict]]: diff_before, diff_after = list[dict](), list[dict]() @@ -265,6 +267,7 @@ def reconcile_host_role_configs( service_name=incoming_role_config["service"], type=incoming_role_config["type"], host_id=host.host_id, + view=view, ).items, ), None, diff --git a/plugins/module_utils/role_utils.py b/plugins/module_utils/role_utils.py index 38730449..afc5d2e9 100644 --- a/plugins/module_utils/role_utils.py +++ b/plugins/module_utils/role_utils.py @@ -112,6 +112,7 @@ def read_role( cluster_name: str, service_name: str, role_name: str, + view: str = "full", ) -> ApiRole: """Read a role for a cluster service and populates the role configuration. @@ -120,6 +121,7 @@ def read_role( cluster_name (str): Cluster name (identifier). service_name (str): Service name (identifier). role_name (str): Role name (identifier). + view (str, optional): View to retrieve. Defaults to 'full'. Raises: ApiException: @@ -132,12 +134,14 @@ def read_role( cluster_name=cluster_name, service_name=service_name, role_name=role_name, + view=view, ) if role is not None: role.config = role_api.read_role_config( cluster_name=cluster_name, service_name=service_name, role_name=role.name, + view=view, ) return role @@ -149,7 +153,7 @@ def read_roles( type: str = None, hostname: str = None, host_id: str = None, - view: str = None, + view: str = "full", ) -> ApiRoleList: """Read roles for a cluster service. Optionally, filter by type, hostname, host ID. @@ -160,7 +164,7 @@ def read_roles( type (str, optional): Role type. Defaults to None. hostname (str, optional): Cluster hostname. Defaults to None. host_id (str, optional): Cluster host ID. Defaults to None. - view (str, optional): View to retrieve. Defaults to None. + view (str, optional): View to retrieve. Defaults to 'full'. Raises: ApiException: @@ -173,11 +177,9 @@ def read_roles( payload = dict( cluster_name=cluster_name, service_name=service_name, + view=view, ) - if view is not None: - payload.update(view=view) - filter = ";".join( [ f"{f[0]}=={f[1]}" @@ -210,11 +212,12 @@ def read_roles_by_type( cluster_name: str, service_name: str, role_type: str, + view: str = "full", ) -> ApiRoleList: role_api = RolesResourceApi(api_client) roles = [ r - for r in role_api.read_roles(cluster_name, service_name).items + for r in role_api.read_roles(cluster_name, service_name, view=view).items if r.type == role_type ] for r in roles: @@ -222,26 +225,27 @@ def read_roles_by_type( cluster_name=cluster_name, service_name=service_name, role_name=r.name, + view=view, ) return ApiRoleList(items=roles) -def read_cm_role(api_client: ApiClient, role_type: str) -> ApiRole: +def read_cm_role(api_client: ApiClient, role_type: str, view: str = "full") -> ApiRole: role_api = MgmtRolesResourceApi(api_client) role = next( iter([r for r in role_api.read_roles().items if r.type == role_type]), None, ) if role is not None: - role.config = role_api.read_role_config(role.name) + role.config = role_api.read_role_config(role_name=role.name, view=view) return role -def read_cm_roles(api_client: ApiClient) -> ApiRoleList: +def read_cm_roles(api_client: ApiClient, view: str = "full") -> ApiRoleList: role_api = MgmtRolesResourceApi(api_client) roles = role_api.read_roles().items for r in roles: - r.config = role_api.read_role_config(role_name=r.name) + r.config = role_api.read_role_config(role_name=r.name, view=view) return ApiRoleList(items=roles) diff --git a/plugins/module_utils/service_utils.py b/plugins/module_utils/service_utils.py index ddfbde90..6d0f3926 100644 --- a/plugins/module_utils/service_utils.py +++ b/plugins/module_utils/service_utils.py @@ -138,6 +138,7 @@ def read_service( api_client: ApiClient, cluster_name: str, service_name: str, + view: str = "summary", ) -> ApiService: """Read a cluster service and its role config group and role dependents. @@ -145,6 +146,7 @@ def read_service( api_client (ApiClient): _description_ cluster_name (str): _description_ service_name (str): _description_ + view (str, optional): View to return. Defaults to 'summary'. Returns: ApiService: _description_ @@ -155,6 +157,7 @@ def read_service( service = service_api.read_service( cluster_name=cluster_name, service_name=service_name, + view=view, ) if service is not None: @@ -162,6 +165,7 @@ def read_service( service.config = service_api.read_service_config( cluster_name=cluster_name, service_name=service_name, + view=view, ) # Gather each role config group configuration @@ -175,17 +179,23 @@ def read_service( api_client=api_client, cluster_name=cluster_name, service_name=service_name, + view=view, ).items return service -def read_services(api_client: ApiClient, cluster_name: str) -> list[ApiService]: +def read_services( + api_client: ApiClient, + cluster_name: str, + view: str = "summary", +) -> list[ApiService]: """Read the cluster services and gather each services' role config group and role dependents. Args: api_client (ApiClient): _description_ cluster_name (str): _description_ + view (str, optional): View to return. Defaults to 'summary'. Returns: ApiService: _description_ @@ -197,6 +207,7 @@ def read_services(api_client: ApiClient, cluster_name: str) -> list[ApiService]: discovered_services = service_api.read_services( cluster_name=cluster_name, + view=view, ).items for service in discovered_services: @@ -204,6 +215,7 @@ def read_services(api_client: ApiClient, cluster_name: str) -> list[ApiService]: service.config = service_api.read_service_config( cluster_name=cluster_name, service_name=service.name, + view=view, ) # Gather each role config group configuration @@ -217,6 +229,7 @@ def read_services(api_client: ApiClient, cluster_name: str) -> list[ApiService]: api_client=api_client, cluster_name=cluster_name, service_name=service.name, + view=view, ).items # Add it to the output @@ -371,11 +384,15 @@ def toggle_service_state( return changed -def read_cm_service(api_client: ApiClient) -> ApiService: +def read_cm_service( + api_client: ApiClient, + view: str = "summary", +) -> ApiService: """Read the Cloudera Manager service and its role config group and role dependents. Args: api_client (ApiClient): _description_ + view: (str, optional): View to return. Defaults to 'summary'. Returns: ApiService: _description_ @@ -384,11 +401,11 @@ def read_cm_service(api_client: ApiClient) -> ApiService: rcg_api = MgmtRoleConfigGroupsResourceApi(api_client) role_api = MgmtRolesResourceApi(api_client) - service = service_api.read_service() + service = service_api.read_service(view=view) if service is not None: # Gather the service-wide configuration - service.config = service_api.read_service_config() + service.config = service_api.read_service_config(view=view) # Gather each role config group configuration service.role_config_groups = [ @@ -398,7 +415,7 @@ def read_cm_service(api_client: ApiClient) -> ApiService: # Gather each role configuration service.roles = role_api.read_roles().items for role in service.roles: - role.config = role_api.read_role_config(role_name=role.name) + role.config = role_api.read_role_config(role_name=role.name, view=view) return service @@ -411,6 +428,7 @@ def reconcile_service_config( check_mode: bool, skip_redacted: bool, message: str, + view: str = "summary", ) -> tuple[dict, dict]: service_api = ServicesResourceApi(api_client) @@ -437,6 +455,7 @@ def _handle_config( existing_config = service_api.read_service_config( cluster_name=service.cluster_ref.cluster_name, service_name=service.name, + view=view, ) (updated_config, before, after) = _handle_config(existing_config) @@ -455,6 +474,7 @@ def _handle_config( config_check = service_api.read_service_config( cluster_name=service.cluster_ref.cluster_name, service_name=service.name, + view=view, ) (_, checked_before, checked_after) = _handle_config(config_check) @@ -497,7 +517,11 @@ def changed(self) -> bool: return bool(self.config.items) -def get_service_hosts(api_client: ApiClient, service: ApiService) -> list[ApiHost]: +def get_service_hosts( + api_client: ApiClient, + service: ApiService, + view: str = "full", +) -> list[ApiHost]: host_api = HostsResourceApi(api_client) seen_hosts = dict() @@ -510,7 +534,10 @@ def get_service_hosts(api_client: ApiClient, service: ApiService) -> list[ApiHos .items ): if r.host_ref.hostname not in seen_hosts: - seen_hosts[r.host_ref.hostname] = host_api.read_host(r.host_ref.host_id) + seen_hosts[r.host_ref.hostname] = host_api.read_host( + host_id=r.host_ref.host_id, + view=view, + ) return seen_hosts.values() @@ -676,6 +703,7 @@ def reconcile_service_roles( message: str, # maintenance: bool, # state: str, + view: str = "full", ) -> tuple[dict, dict]: diff_before, diff_after = list[dict](), list[dict]() @@ -696,6 +724,7 @@ def reconcile_service_roles( cluster_name=service.cluster_ref.cluster_name, service_name=service.name, role_type=incoming_role["type"], + view=view, ).items # Get the base role config group for the type diff --git a/plugins/modules/cluster.py b/plugins/modules/cluster.py index 764de79e..10731083 100644 --- a/plugins/modules/cluster.py +++ b/plugins/modules/cluster.py @@ -366,6 +366,16 @@ default: no aliases: - auto_assign_roles + view: + description: + - View type of the returned cluster details. + type: str + required: false + choices: + - summary + - full + - export + default: summary extends_documentation_fragment: - ansible.builtin.action_common_attributes - cloudera.cluster.cm_options @@ -838,6 +848,7 @@ def __init__(self, module): self.control_plane = self.get_param("control_plane") self.auto_tls = self.get_param("auto_tls") self.force = self.get_param("force") + self.view = self.get_param("view") self.changed = False self.output = {} @@ -1716,6 +1727,7 @@ def main(): force=dict(type="bool", aliases=["forced_init"]), # Optional auto-assign roles on cluster (honors existing assignments) auto_assign=dict(type="bool", default=False, aliases=["auto_assign_roles"]), + view=dict(choices=["summary", "full", "export"], default="summary"), ), supports_check_mode=True, mutually_exclusive=[ diff --git a/plugins/modules/cluster_info.py b/plugins/modules/cluster_info.py index c0a5a4b2..6cba69fd 100644 --- a/plugins/modules/cluster_info.py +++ b/plugins/modules/cluster_info.py @@ -29,7 +29,7 @@ - Name of Cloudera Manager cluster. - This parameter specifies the name of the cluster from which data will be gathered. type: str - required: False + required: false requirements: - cm_client """ diff --git a/plugins/modules/cm_config.py b/plugins/modules/cm_config.py index 7a1d0fb1..faed740d 100644 --- a/plugins/modules/cm_config.py +++ b/plugins/modules/cm_config.py @@ -26,14 +26,25 @@ requirements: - cm_client options: - parameters: + config: description: - The Cloudera Manager configuration to set. - To unset a parameter, use C(None) as the value. type: dict required: yes aliases: + - parameters - params + view: + description: + - View type of the returned Cloudera Manager details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - ansible.builtin.action_common_attributes - cloudera.cluster.cm_options @@ -160,8 +171,9 @@ def __init__(self, module): super(ClouderaManagerConfig, self).__init__(module) # Set the parameters - self.params = self.get_param("parameters") + self.config = self.get_param("config") self.purge = self.get_param("purge") + self.view = self.get_param("view") # Initialize the return value self.changed = False @@ -173,11 +185,10 @@ def __init__(self, module): @ClouderaManagerMutableModule.handle_process def process(self): - refresh = True - existing = self.get_cm_config() + existing = self.get_cm_config(scope=self.view) current = {r.name: r.value for r in existing} - incoming = {k.upper(): v for k, v in self.params.items()} + incoming = {k.upper(): v for k, v in self.config.items()} change_set = resolve_parameter_changeset(current, incoming, self.purge) @@ -197,8 +208,6 @@ def process(self): for k, v in change_set.items() ], ) - # Return 'summary' - refresh = False self.config = [ p.to_dict() for p in cm_client.ClouderaManagerResourceApi(self.api_client) @@ -206,16 +215,17 @@ def process(self): .items ] - if refresh: - # Return 'summary' - self.config = [p.to_dict() for p in self.get_cm_config()] + self.config = [p.to_dict() for p in self.get_cm_config(scope=self.view)] + else: + self.config = [p.to_dict() for p in existing] def main(): module = ClouderaManagerMutableModule.ansible_module( argument_spec=dict( - parameters=dict(type=dict, required=True, aliases=["params"]), + config=dict(type=dict, required=True, aliases=["parameters", "params"]), purge=dict(type="bool", default=False), + view=dict(choices=["summary", "full", "export"], default="summary"), ), supports_check_mode=True, ) diff --git a/plugins/modules/cm_service.py b/plugins/modules/cm_service.py index e2838be5..7d7fd1b1 100644 --- a/plugins/modules/cm_service.py +++ b/plugins/modules/cm_service.py @@ -121,6 +121,16 @@ - present - restarted required: no + view: + description: + - View type of the returned service details. + type: str + required: false + choices: + - summary + - full + - export + default: summary extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -597,6 +607,7 @@ def __init__(self, module): self.roles = self.get_param("roles") self.state = self.get_param("state") self.purge = self.get_param("purge") + self.view = self.get_param("view") # Initialize the return value self.changed = False @@ -623,7 +634,7 @@ def process(self): # Discover the CM service and retrieve its configured dependents try: - current = read_cm_service(self.api_client) + current = read_cm_service(api_client=self.api_client, view=self.view) except ApiException as ex: if ex.status != 404: raise ex @@ -647,7 +658,7 @@ def process(self): self.changed = True new_service = ApiService(type="MGMT") current = service_api.setup_cms(body=new_service) - current.config = service_api.read_service_config() + current.config = service_api.read_service_config(view=self.view) current.role_config_groups = [] current.roles = [] @@ -999,7 +1010,7 @@ def process(self): # If there are changes, get a fresh read if self.changed: - refresh = read_cm_service(self.api_client) + refresh = read_cm_service(api_client=self.api_client, view=self.view) self.output = parse_service_result(refresh) # Otherwise, return the existing else: @@ -1066,6 +1077,10 @@ def main(): default="present", choices=["started", "stopped", "absent", "present", "restarted"], ), + view=dict( + default="summary", + choices=["summary", "full"], + ), ), supports_check_mode=True, ) diff --git a/plugins/modules/cm_service_config.py b/plugins/modules/cm_service_config.py index dd89a232..bc89f7a7 100644 --- a/plugins/modules/cm_service_config.py +++ b/plugins/modules/cm_service_config.py @@ -26,13 +26,14 @@ requirements: - cm-client options: - parameters: + config: description: - The service-wide configuration to set. - To unset a parameter, use C(None) as the value. type: dict required: yes aliases: + - parameters - params view: description: @@ -198,7 +199,7 @@ def __init__(self, module): super(ClouderaManagerServiceConfig, self).__init__(module) # Set the parameters - self.params = self.get_param("parameters") + self.config = self.get_param("config") self.purge = self.get_param("purge") self.view = self.get_param("view") @@ -223,7 +224,7 @@ def process(self): else: raise ex - updates = ServiceConfigUpdates(existing, self.params, self.purge) + updates = ServiceConfigUpdates(existing, self.config, self.purge) if updates.changed: self.changed = True @@ -253,7 +254,7 @@ def process(self): def main(): module = ClouderaManagerMutableModule.ansible_module( argument_spec=dict( - parameters=dict(type="dict", required=True, aliases=["params"]), + config=dict(type="dict", required=True, aliases=["parameters", "params"]), purge=dict(type="bool", default=False), view=dict( default="summary", diff --git a/plugins/modules/cm_service_info.py b/plugins/modules/cm_service_info.py index a466ef64..08e14b53 100644 --- a/plugins/modules/cm_service_info.py +++ b/plugins/modules/cm_service_info.py @@ -24,6 +24,17 @@ - Ronald Suplina (@rsuplina) - Webster Mudge (@wmudge) version_added: "4.4.0" +options: + view: + description: + - View type of the returned service details. + type: str + required: false + choices: + - summary + - full + - export + default: summary extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -352,6 +363,9 @@ class ClouderaServiceInfo(ClouderaManagerModule): def __init__(self, module): super(ClouderaServiceInfo, self).__init__(module) + # Set the parameters + self.view = self.get_param("view") + # Initialize the return values self.output = dict() @@ -362,7 +376,7 @@ def __init__(self, module): def process(self): result = None try: - result = read_cm_service(self.api_client) + result = read_cm_service(api_client=self.api_client, view=self.view) except ApiException as ex: if ex.status != 404: raise ex @@ -372,7 +386,15 @@ def process(self): def main(): - module = ClouderaManagerModule.ansible_module(supports_check_mode=True) + module = ClouderaManagerModule.ansible_module( + argument_spec=dict( + view=dict( + default="summary", + choices=["summary", "full"], + ), + ), + supports_check_mode=True, + ) result = ClouderaServiceInfo(module) diff --git a/plugins/modules/cm_service_role.py b/plugins/modules/cm_service_role.py index 4fc62026..689e5028 100644 --- a/plugins/modules/cm_service_role.py +++ b/plugins/modules/cm_service_role.py @@ -78,6 +78,16 @@ - restarted - started - stopped + view: + description: + - View type of the returned role details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -351,6 +361,7 @@ def __init__(self, module): self.type = self.get_param("type") self.state = self.get_param("state") self.purge = self.get_param("purge") + self.view = self.get_param("view") # Initialize the return values self.changed = False @@ -380,7 +391,11 @@ def process(self): # Discover the role by its type try: - current = read_cm_role(api_client=self.api_client, role_type=self.type) + current = read_cm_role( + api_client=self.api_client, + role_type=self.type, + view=self.view, + ) except ApiException as ex: if ex.status != 404: raise ex @@ -481,8 +496,11 @@ def process(self): # If there are changes, get a fresh read if self.changed: - refresh = role_api.read_role(current.name) - refresh.config = role_api.read_role_config(current.name) + refresh = role_api.read_role(role_name=current.name) + refresh.config = role_api.read_role_config( + role_name=current.name, + view=self.view, + ) self.output = parse_role_result(refresh) # Otherwise return the existing else: @@ -609,6 +627,7 @@ def main(): default="present", choices=["present", "absent", "restarted", "started", "stopped"], ), + view=dict(choices=["summary", "full", "export"], default="full"), ), mutually_exclusive=[ ["cluster_hostname", "cluster_host_id"], diff --git a/plugins/modules/cm_service_role_info.py b/plugins/modules/cm_service_role_info.py index 94829b45..b3fb204f 100644 --- a/plugins/modules/cm_service_role_info.py +++ b/plugins/modules/cm_service_role_info.py @@ -30,6 +30,16 @@ type: str aliases: - role_type + view: + description: + - View type of the returned role details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -228,6 +238,7 @@ def __init__(self, module): # Set the parameters self.type = self.get_param("type") + self.view = self.get_param("view") # Initialize the return values self.output = list() @@ -251,7 +262,11 @@ def process(self): result = None try: - result = read_cm_role(api_client=self.api_client, role_type=self.type) + result = read_cm_role( + api_client=self.api_client, + role_type=self.type, + view=self.view, + ) except ApiException as ex: if ex.status != 404: raise ex @@ -261,7 +276,7 @@ def process(self): else: self.output = [ parse_role_result(r) - for r in read_cm_roles(api_client=self.api_client).items + for r in read_cm_roles(api_client=self.api_client, view=self.view).items ] @@ -269,6 +284,7 @@ def main(): module = ClouderaManagerModule.ansible_module( argument_spec=dict( type=dict(aliases=["role_type"]), + view=dict(choices=["summary", "full", "export"], default="full"), ), supports_check_mode=False, ) diff --git a/plugins/modules/external_account.py b/plugins/modules/external_account.py index 8a9bc6f1..68f8c992 100644 --- a/plugins/modules/external_account.py +++ b/plugins/modules/external_account.py @@ -17,7 +17,7 @@ DOCUMENTATION = r""" module: external_account -short_description: Create, update, or delete an external module account +short_description: Create, update, or delete an external account description: - Manage external accounts, including creation, updates, and deletion. - Supports a variety of account types such as AWS, Azure, Altus, and Basic Authentication. @@ -29,25 +29,15 @@ options: name: description: - - The initial name of the account. + - The name of the account. type: str - required: no - category: - description: - - The category of the account. - type: str - required: no - choices: - - AWS - - ALTUS - - AZURE - - BASICAUTH + required: true state: description: - If O(state=present), the account will be created or updated. - If O(state=absent), the account will be deleted. type: str - required: no + required: false default: present choices: - present @@ -56,19 +46,19 @@ description: - The type of the external account. type: str - required: no + required: false choices: - AWS_ACCESS_KEY_AUTH - AWS_IAM_ROLES_AUTH - ALTUS_ACCESS_KEY_AUTH - ADLS_AD_SVC_PRINC_AUTH - BASIC_AUTH - params: + config: description: - A dictionary of parameters for the external account configuration. - The required parameters depend on the type of the account. type: dict - required: no + required: false suboptions: aws_access_key: description: @@ -106,6 +96,19 @@ description: - The password for BASIC_AUTH. type: str + aliases: + - parameters + - params + view: + description: + - View type of the returned external account details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -121,7 +124,7 @@ """ EXAMPLES = r""" -- name: Create AWS Access key credentials +- name: Create an AWS external account cloudera.cluster.external_account: host: example.cloudera.com port: "7180" @@ -129,27 +132,25 @@ password: "S&peR4Ec*re" name: access_key_1 state: present - type: AWS - category: AWS_ACCESS_KEY_AUTH + type: AWS_ACCESS_KEY_AUTH params: aws_access_key: access_key1 aws_secret_key: secret_key1 -- name: Create basic authentication credentials +- name: Create basic authentication external account cloudera.cluster.external_account: host: example.cloudera.com port: "7180" username: "jane_smith" password: "S&peR4Ec*re" - name: Jane + name: JaneDoe state: present - type: BASIC_AUTH - category: BASICAUTH + type: BASICAUTH params: username: jane_user password: pass123! -- name: Update AWS Access key credentials +- name: Update an AWS external account cloudera.cluster.external_account: host: example.cloudera.com port: "7180" @@ -157,19 +158,18 @@ password: "S&peR4Ec*re" name: access_key_1 state: present - type: AWS - category: AWS_ACCESS_KEY_AUTH + type: AWS_ACCESS_KEY_AUTH params: aws_access_key: access_key2 aws_secret_key: secret_key2 -- name: Remove basic authentication credentials +- name: Remove an external account cloudera.cluster.external_account: host: example.cloudera.com port: "7180" username: "jane_smith" password: "S&peR4Ec*re" - name: Jane + name: JaneDoe state: absent """ @@ -177,11 +177,10 @@ external_account: description: Details of the external account created, updated, or retrieved. type: dict - elements: complex returned: always contains: name: - description: Represents the initial name of the account. + description: The name of the account. type: str returned: always display_name: @@ -220,6 +219,14 @@ ApiConfigList, ) +ACCOUNT_TYPES = [ + "AWS_ACCESS_KEY_AUTH", + "AWS_IAM_ROLES_AUTH", + "ALTUS_ACCESS_KEY_AUTH", + "ADLS_AD_SVC_PRINC_AUTH", + "BASIC_AUTH", +] + class ClouderaExternalAccount(ClouderaManagerModule): def __init__(self, module): @@ -227,13 +234,13 @@ def __init__(self, module): # Set the parameters self.name = self.get_param("name") - self.category = self.get_param("category") self.type = self.get_param("type") - self.params = self.get_param("params") + self.config = self.get_param("config") self.state = self.get_param("state") + self.view = self.get_param("view") # Initialize the return values - self.external_account = [] + self.external_account = {} self.changed = False if self.module._diff: @@ -250,21 +257,23 @@ def __init__(self, module): def process(self): api_instance = ExternalAccountsResourceApi(self.api_client) existing = [] - self.params = { - key: value for key, value in self.params.items() if value is not None + + self.config = { + key: value for key, value in self.config.items() if value is not None } + try: - existing = api_instance.read_account(self.name).to_dict() + existing = api_instance.read_account( + name=self.name, + view=self.view, + ).to_dict() except ApiException as ex: - if ex.status == 400: - pass - else: + if ex.status != 400: raise ex if self.state == "present": try: if existing: - if self.module._diff: self.before.update(existing) self.after.update( @@ -272,7 +281,7 @@ def process(self): display_name=self.name, type_name=self.type, account_configs={ - key: value for key, value in self.params.items() + key: value for key, value in self.config.items() }, ) if self.before != self.after: @@ -288,7 +297,7 @@ def process(self): account_configs=ApiConfigList( items=[ ApiConfig(name=key, value=value) - for key, value in self.params.items() + for key, value in self.config.items() ], ), ), @@ -302,7 +311,7 @@ def process(self): "display_name": self.name, "type_name": self.type, "account_configs": { - key: value for key, value in self.params.items() + key: value for key, value in self.config.items() }, } if not self.module.check_mode: @@ -314,7 +323,7 @@ def process(self): account_configs=ApiConfigList( items=[ ApiConfig(name=key, value=value) - for key, value in self.params.items() + for key, value in self.config.items() ], ), ), @@ -339,11 +348,6 @@ def main(): module = ClouderaManagerModule.ansible_module( argument_spec=dict( name=dict(required=True, type="str"), - category=dict( - type="str", - required=False, - choices=["AWS", "ALTUS", "AZURE", "BASICAUTH"], - ), state=dict( type="str", default="present", @@ -352,15 +356,9 @@ def main(): type=dict( type="str", required=False, - choices=[ - "AWS_ACCESS_KEY_AUTH", - "AWS_IAM_ROLES_AUTH", - "ALTUS_ACCESS_KEY_AUTH", - "ADLS_AD_SVC_PRINC_AUTH", - "BASIC_AUTH", - ], + choices=ACCOUNT_TYPES, ), - params=dict( + config=dict( type="dict", default={}, options=dict( @@ -378,27 +376,39 @@ def main(): username=dict(type="str"), password=dict(type="str"), ), - ), - required_if={ - "AWS_ACCESS_KEY_AUTH": [ - "params.aws_access_key", - "params.aws_secret_key", + # TODO Update to use custom validation for nested dependencies + required_if=[ + [ + "type", + "AWS_ACCESS_KEY_AUTH", + ["aws_access_key", "aws_secret_key"], + False, + ], + [ + "type", + "ADLS_AD_SVC_PRINC_AUTH", + ["adls_client_id", "adls_client_id", "adls_tenant_id"], + False, + ], + [ + "type", + "ALTUS_ACCESS_KEY_AUTH", + ["access_key_id", "private_key"], + False, + ], + ["type", "BASIC_AUTH", ["username", "password"], False], ], - "ADLS_AD_SVC_PRINC_AUTH": [ - "params.adls_client_id", - "params.adls_client_id", - "params.adls_tenant_id", - ], - "ALTUS_ACCESS_KEY_AUTH": [ - "params.access_key_id", - "params.private_key", - ], - "BASIC_AUTH": [ - "params.username", - "params.password", - ], - }, + aliases=["parameters", "params"], + ), + view=dict(choices=["summary", "full", "export"], default="full"), ), + required_if=[ + ["state", "present", ["type"], False], + # ["type", "AWS_ACCESS_KEY_AUTH", ["config.aws_access_key", "config.aws_secret_key"], False], + # ["type", "ADLS_AD_SVC_PRINC_AUTH", ["config.adls_client_id", "config.adls_client_id", "config.adls_tenant_id"], False], + # ["type", "ALTUS_ACCESS_KEY_AUTH", ["config.access_key_id", "config.private_key"], False], + # ["type", "BASIC_AUTH", ["config.username", "config.password"], False], + ], supports_check_mode=True, ) diff --git a/plugins/modules/external_account_info.py b/plugins/modules/external_account_info.py index cc88d2b2..29625037 100644 --- a/plugins/modules/external_account_info.py +++ b/plugins/modules/external_account_info.py @@ -40,6 +40,16 @@ - ALTUS_ACCESS_KEY_AUTH - ADLS_AD_SVC_PRINC_AUTH - BASIC_AUTH + view: + description: + - View type of the returned external account details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -120,6 +130,14 @@ ExternalAccountsResourceApi, ) +ACCOUNT_TYPES = [ + "AWS_ACCESS_KEY_AUTH", + "AWS_IAM_ROLES_AUTH", + "ALTUS_ACCESS_KEY_AUTH", + "ADLS_AD_SVC_PRINC_AUTH", + "BASIC_AUTH", +] + class ClouderaExternalAccountInfo(ClouderaManagerModule): def __init__(self, module): @@ -128,6 +146,7 @@ def __init__(self, module): # Set the parameters self.name = self.get_param("name") self.type = self.get_param("type") + self.view = self.get_param("view") # Initialize the return values self.external_accounts = [] @@ -139,38 +158,29 @@ def __init__(self, module): @ClouderaManagerModule.handle_process def process(self): api_instance = ExternalAccountsResourceApi(self.api_client) - account_types = [ - "AWS_ACCESS_KEY_AUTH", - "AWS_IAM_ROLES_AUTH", - "ALTUS_ACCESS_KEY_AUTH", - "ADLS_AD_SVC_PRINC_AUTH", - "BASIC_AUTH", - ] + try: if self.name: self.external_accounts = [ - api_instance.read_account(self.name).to_dict(), + api_instance.read_account(name=self.name, view=self.view).to_dict(), ] - elif self.type: - self.external_accounts = ( - api_instance.read_accounts(self.type).to_dict().get("items", []) - ) - + self.external_accounts = [ + a.to_dict() + for a in api_instance.read_accounts( + type_name=self.type, + view=self.view, + ).items + ] else: - - self.external_accounts = api_instance.read_accounts( - type_name="AWS_ACCESS_KEY_AUTH", - ).to_dict()["items"] - all_accounts = [] - for account_type in account_types: - accounts = ( - api_instance.read_accounts(type_name=account_type) - .to_dict() - .get("items", []) - ) - all_accounts.extend(accounts) - self.external_accounts = all_accounts + self.external_accounts = [ + a.to_dict() + for t in ACCOUNT_TYPES + for a in api_instance.read_accounts( + type_name=t, + view=self.view, + ).items + ] except ApiException as e: if e.status == 404: @@ -181,20 +191,11 @@ def process(self): def main(): module = ClouderaManagerModule.ansible_module( argument_spec=dict( - name=dict(required=False, type="str"), - type=dict( - type="str", - required=False, - choices=[ - "AWS_ACCESS_KEY_AUTH", - "AWS_IAM_ROLES_AUTH", - "ALTUS_ACCESS_KEY_AUTH", - "ADLS_AD_SVC_PRINC_AUTH", - "BASIC_AUTH", - ], - ), + name=dict(), + type=dict(choices=ACCOUNT_TYPES), + view=dict(choices=["summary", "full", "export"], default="full"), ), - supports_check_mode=False, + supports_check_mode=True, ) result = ClouderaExternalAccountInfo(module) diff --git a/plugins/modules/external_user_mappings.py b/plugins/modules/external_user_mappings.py index 608b7e9d..75174135 100644 --- a/plugins/modules/external_user_mappings.py +++ b/plugins/modules/external_user_mappings.py @@ -68,6 +68,16 @@ - If I(purge=False), the provided authorization roles will be added to the existing ones, and any duplicates will be ignored. type: bool default: False + view: + description: + - View type of the returned external user mapping details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -192,6 +202,7 @@ def __init__(self, module): self.type = self.get_param("type") self.purge = self.get_param("purge") self.auth_roles = self.get_param("auth_roles") + self.view = self.get_param("view") # Initialize the return value self.external_user_mappings_output = [] @@ -207,7 +218,9 @@ def process(self): existing = [] if self.name: - all_external_user_mappings = api_instance.read_external_user_mappings() + all_external_user_mappings = api_instance.read_external_user_mappings( + view=self.view, + ) for mapping in all_external_user_mappings.items: if self.name == mapping.name: existing = api_instance.read_external_user_mapping( @@ -324,6 +337,7 @@ def main(): default="present", choices=["present", "absent"], ), + view=dict(choices=["summary", "full", "export"], default="full"), ), supports_check_mode=True, required_one_of=[ diff --git a/plugins/modules/external_user_mappings_info.py b/plugins/modules/external_user_mappings_info.py index 9d31040c..9370af89 100644 --- a/plugins/modules/external_user_mappings_info.py +++ b/plugins/modules/external_user_mappings_info.py @@ -30,12 +30,22 @@ description: - The name of the external mapping. type: str - required: no + required: false uuid: description: - The uuid of the external mapping. type: str - required: no + required: false + view: + description: + - View type of the returned external user mapping details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -110,6 +120,7 @@ def __init__(self, module): # Set the parameters self.name = self.get_param("name") self.uuid = self.get_param("uuid") + self.view = self.get_param("view") # Initialize the return value self.external_user_mappings_info_output = [] @@ -123,7 +134,9 @@ def process(self): api_instance = ExternalUserMappingsResourceApi(self.api_client) try: if self.name: - external_user_mappings = api_instance.read_external_user_mappings() + external_user_mappings = api_instance.read_external_user_mappings( + view=self.view, + ) for mapping in external_user_mappings.items: if self.name == mapping.name: self.external_user_mappings_info_output = [ @@ -137,7 +150,9 @@ def process(self): ] else: self.external_user_mappings_info_output = ( - api_instance.read_external_user_mappings().to_dict()["items"] + api_instance.read_external_user_mappings(view=self.view).to_dict()[ + "items" + ] ) except ApiException as ex: if ex.status != 400: @@ -147,8 +162,9 @@ def process(self): def main(): module = ClouderaManagerModule.ansible_module( argument_spec=dict( - name=dict(required=False, type="str"), - uuid=dict(required=False, type="str"), + name=dict(), + uuid=dict(), + view=dict(choices=["summary", "full", "export"], default="full"), ), supports_check_mode=True, mutually_exclusive=[ diff --git a/plugins/modules/host.py b/plugins/modules/host.py index 041a2a79..3e20c1b3 100644 --- a/plugins/modules/host.py +++ b/plugins/modules/host.py @@ -161,6 +161,16 @@ - started - stopped - restarted + view: + description: + - View type of the returned host details. + type: str + required: false + choices: + - summary + - full + - export + default: full timeout: description: - Timeout, in seconds, before failing when joining a cluster. @@ -415,6 +425,7 @@ def __init__(self, module): self.state = self.get_param("state") self.timeout = self.get_param("timeout") self.delay = self.get_param("delay") + self.view = self.get_param("view") # Initialize the return values self.output = {} @@ -439,7 +450,7 @@ def process(self): api_client=self.api_client, hostname=self.name, host_id=self.host_id, - view="full", + view=self.view, ) except ApiException as ex: if ex.status != 404: @@ -767,6 +778,7 @@ def process(self): check_mode=self.module.check_mode, skip_redacted=self.skip_redacted, message=self.message, + view=self.view, ) except HostException as he: self.module.fail_json(msg=to_native(he)) @@ -872,6 +884,7 @@ def main(): "restarted", ], ), + view=dict(choices=["summary", "full", "export"], default="full"), timeout=dict(type="int", default=300, aliases=["polling_timeout"]), delay=dict(type="int", default=15, aliases=["polling_interval"]), ), diff --git a/plugins/modules/host_info.py b/plugins/modules/host_info.py index bcff3bb5..3123cc40 100644 --- a/plugins/modules/host_info.py +++ b/plugins/modules/host_info.py @@ -44,6 +44,16 @@ - The unique identifier of the host. type: str required: no + view: + description: + - View type of the returned host details. + type: str + required: false + choices: + - summary + - full + - export + default: full extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -238,6 +248,7 @@ def __init__(self, module): self.cluster = self.get_param("cluster") self.name = self.get_param("name") self.host_id = self.get_param("host_id") + self.view = self.get_param("view") # Initialize the return values self.output = [] @@ -255,13 +266,17 @@ def process(self): if self.host_id: try: - hosts.append(host_api.read_host(host_id=self.host_id)) + hosts.append(host_api.read_host(host_id=self.host_id, view=self.view)) except ApiException as ex: if ex.status != 404: raise ex elif self.name: host = next( - (h for h in host_api.read_hosts().items if h.hostname == self.name), + ( + h + for h in host_api.read_hosts(view=self.view).items + if h.hostname == self.name + ), None, ) if host is not None: @@ -279,10 +294,10 @@ def process(self): cluster_name=self.cluster, ).items else: - hosts = host_api.read_hosts().items + hosts = host_api.read_hosts(view=self.view).items for host in hosts: - host.config = host_api.read_host_config(host.host_id) + host.config = host_api.read_host_config(host.host_id, view=self.view) self.output.append(parse_host_result(host)) @@ -292,6 +307,7 @@ def main(): cluster=dict(aliases=["cluster_name"]), name=dict(aliases=["cluster_hostname"]), host_id=dict(), + view=dict(choices=["summary", "full", "export"], default="full"), ), supports_check_mode=True, ) diff --git a/plugins/modules/role.py b/plugins/modules/role.py index 2069b48c..0b80d136 100644 --- a/plugins/modules/role.py +++ b/plugins/modules/role.py @@ -119,6 +119,19 @@ - restarted - started - stopped + view: + description: + - The view to materialize. + - C(healthcheck) is the equivalent to I(full_with_health_check_explanation). + - C(redacted) is the equivalent to I(export_redacted). + type: str + default: summary + choices: + - summary + - full + - healthcheck + - export + - redacted extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -407,6 +420,7 @@ def __init__(self, module): self.state = self.get_param("state") self.purge = self.get_param("purge") self.skip_redacted = self.get_param("skip_redacted") + self.view = self.get_param("view") # Initialize the return values self.changed = False @@ -444,10 +458,15 @@ def process(self): else: raise ex + if self.view == "healthcheck": + self.view = "full_with_health_check_explanation" + elif self.view == "redacted": + self.view = "export_redacted" + role_api = RolesResourceApi(self.api_client) current = None - # If given the role identifier, get it or fail (is a read-only variable) + # If given the role identifier, get it or return nothing (is a read-only variable) if self.name: try: current = read_role( @@ -455,6 +474,7 @@ def process(self): cluster_name=self.cluster, service_name=self.service, role_name=self.name, + view=self.view, ) except ApiException as ex: if ex.status != 404: @@ -472,6 +492,7 @@ def process(self): type=self.type, hostname=self.cluster_hostname, host_id=self.cluster_host_id, + view=self.view, ).items, ), None, @@ -672,6 +693,7 @@ def process(self): cluster_name=self.cluster, service_name=self.service, role_name=current.name, + view=self.view, ), ) else: @@ -718,6 +740,10 @@ def main(): default="present", choices=["present", "absent", "restarted", "started", "stopped"], ), + view=dict( + default="summary", + choices=["summary", "full", "healthcheck", "export", "redacted"], + ), ), mutually_exclusive=[ ["type", "name"], diff --git a/plugins/modules/role_info.py b/plugins/modules/role_info.py index 6221612a..cbc2a436 100644 --- a/plugins/modules/role_info.py +++ b/plugins/modules/role_info.py @@ -323,6 +323,7 @@ def process(self): cluster_name=self.cluster, service_name=self.service, role_name=self.role, + view=self.view, ), ), ) diff --git a/plugins/modules/service.py b/plugins/modules/service.py index 64e26b78..0fc61ec3 100644 --- a/plugins/modules/service.py +++ b/plugins/modules/service.py @@ -176,6 +176,19 @@ - restarted - started - stopped + view: + description: + - The view to materialize. + - C(healthcheck) is the equivalent to I(full_with_health_check_explanation). + - C(redacted) is the equivalent to I(export_redacted). + type: str + default: summary + choices: + - summary + - full + - healthcheck + - export + - redacted extends_documentation_fragment: - ansible.builtin.action_common_attributes - cloudera.cluster.cm_options @@ -701,6 +714,7 @@ def __init__(self, module): self.roles = self.get_param("roles") self.role_config_groups = self.get_param("role_config_groups") self.state = self.get_param("state") + self.view = self.get_param("view") # Initialize the return values self.changed = False @@ -720,6 +734,11 @@ def process(self): else: raise ex + if self.view == "healthcheck": + self.view = "full_with_health_check_explanation" + elif self.view == "redacted": + self.view = "export_redacted" + service_api = ServicesResourceApi(self.api_client) current = None @@ -729,6 +748,7 @@ def process(self): api_client=self.api_client, cluster_name=self.cluster, service_name=self.name, + view=self.view, ) except ApiException as ex: if ex.status != 404: @@ -932,6 +952,7 @@ def process(self): check_mode=self.module.check_mode, skip_redacted=self.skip_redacted, message=self.message, + view=self.view, ) if before_config or after_config: @@ -1021,6 +1042,7 @@ def process(self): message=self.message, # state=self.state, # maintenance=self.maintenance, + view=self.view, ) if before_role or after_role: @@ -1121,6 +1143,10 @@ def main(): default="present", choices=["present", "absent", "started", "stopped", "restarted"], ), + view=dict( + default="summary", + choices=["summary", "full", "healthcheck", "export", "redacted"], + ), ), supports_check_mode=True, ) diff --git a/plugins/modules/service_info.py b/plugins/modules/service_info.py index f4a196a8..482c10d1 100644 --- a/plugins/modules/service_info.py +++ b/plugins/modules/service_info.py @@ -39,6 +39,19 @@ aliases: - service_name - service + view: + description: + - The view to materialize. + - C(healthcheck) is the equivalent to I(full_with_health_check_explanation). + - C(redacted) is the equivalent to I(export_redacted). + type: str + default: summary + choices: + - summary + - full + - healthcheck + - export + - redacted extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint @@ -394,6 +407,11 @@ def process(self): else: raise ex + if self.view == "healthcheck": + self.view = "full_with_health_check_explanation" + elif self.view == "redacted": + self.view = "export_redacted" + if self.name: try: self.output.append( @@ -402,6 +420,7 @@ def process(self): api_client=self.api_client, cluster_name=self.cluster, service_name=self.name, + view=self.view, ), ), ) @@ -414,6 +433,7 @@ def process(self): for s in read_services( api_client=self.api_client, cluster_name=self.cluster, + view=self.view, ) ] @@ -423,6 +443,10 @@ def main(): argument_spec=dict( cluster=dict(required=True, aliases=["cluster_name"]), name=dict(aliases=["service_name", "service"]), + view=dict( + default="summary", + choices=["summary", "full", "healthcheck", "export", "redacted"], + ), ), supports_check_mode=True, ) From 4f914c5b56b5009b1d1523b919210ebf09729145 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Tue, 29 Jul 2025 16:17:06 -0400 Subject: [PATCH 2/9] Update documentation for control_plane modules linting Signed-off-by: Webster Mudge --- plugins/modules/control_plane.py | 1 + plugins/modules/control_plane_info.py | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/modules/control_plane.py b/plugins/modules/control_plane.py index e1352c29..c23c941e 100755 --- a/plugins/modules/control_plane.py +++ b/plugins/modules/control_plane.py @@ -52,6 +52,7 @@ extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint + - ansible.builtin.action_common_attributes attributes: check_mode: support: full diff --git a/plugins/modules/control_plane_info.py b/plugins/modules/control_plane_info.py index 764808cc..b25b87de 100644 --- a/plugins/modules/control_plane_info.py +++ b/plugins/modules/control_plane_info.py @@ -26,6 +26,7 @@ extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint + - ansible.builtin.action_common_attributes attributes: check_mode: support: full From 0bb7db736f6ed9ca5c89af18d00bcc788c85238d Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Tue, 29 Jul 2025 16:17:24 -0400 Subject: [PATCH 3/9] Add ansible-lint configuration Signed-off-by: Webster Mudge --- .ansible-lint | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .ansible-lint diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 00000000..6bbceaa3 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,19 @@ +# 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. + +profile: production +quiet: true +strict: true +verbosity: 1 +offline: true From d17d25242bd3974e96ebaaf2f98d8aadfdf63161 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Tue, 29 Jul 2025 16:17:38 -0400 Subject: [PATCH 4/9] Remove ansible-lint from pre-commit hooks Signed-off-by: Webster Mudge --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8c32c3f..f2d52ac3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,8 +46,8 @@ repos: hooks: - id: black name: lint python - - repo: https://github.com/ansible/ansible-lint - rev: v25.6.1 - hooks: - - id: ansible-lint - name: lint ansible + # - repo: https://github.com/ansible/ansible-lint + # rev: v25.6.1 + # hooks: + # - id: ansible-lint + # name: lint ansible From c32aaf300c6c8881fa4b6e42e42df47600f38e58 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 31 Jul 2025 09:31:30 -0400 Subject: [PATCH 5/9] Set default view to 'full' for CM config Signed-off-by: Webster Mudge --- plugins/module_utils/cm_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/cm_utils.py b/plugins/module_utils/cm_utils.py index 3e2a0a6c..ee1e8be3 100644 --- a/plugins/module_utils/cm_utils.py +++ b/plugins/module_utils/cm_utils.py @@ -623,7 +623,7 @@ def call_api(self, path, method, query=None, field="items", body=None): data = data[field] return data if isinstance(data, list) else [data] - def get_cm_config(self, scope: str = "summary") -> list[ApiConfig]: + def get_cm_config(self, scope: str = "full") -> list[ApiConfig]: return ClouderaManagerResourceApi(self.api_client).get_config(view=scope).items def wait_command(self, command: ApiCommand, polling: int = 10, delay: int = 5): From b0bded948a3c51319c98796a35300db16c9e4f72 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 31 Jul 2025 09:32:23 -0400 Subject: [PATCH 6/9] Clean up cm_config output and execution logic Signed-off-by: Webster Mudge --- plugins/modules/cm_config.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/plugins/modules/cm_config.py b/plugins/modules/cm_config.py index faed740d..47a71d54 100644 --- a/plugins/modules/cm_config.py +++ b/plugins/modules/cm_config.py @@ -158,7 +158,11 @@ returned: when supported """ -import cm_client +from cm_client import ( + ApiConfig, + ApiConfigList, + ClouderaManagerResourceApi, +) from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( ClouderaManagerMutableModule, @@ -172,19 +176,21 @@ def __init__(self, module): # Set the parameters self.config = self.get_param("config") - self.purge = self.get_param("purge") - self.view = self.get_param("view") + self.purge = bool(self.get_param("purge")) + self.view = str(self.get_param("view")) # Initialize the return value self.changed = False self.diff = {} - self.config = [] + self.output = [] # Execute the logic self.process() @ClouderaManagerMutableModule.handle_process def process(self): + cm_api = ClouderaManagerResourceApi(api_client=self.api_client) + existing = self.get_cm_config(scope=self.view) current = {r.name: r.value for r in existing} @@ -202,18 +208,13 @@ def process(self): ) if not self.module.check_mode: - body = cm_client.ApiConfigList( + body = ApiConfigList( items=[ - cm_client.ApiConfig(name=k, value=v) + ApiConfig(name=k, value=v) for k, v in change_set.items() ], ) - self.config = [ - p.to_dict() - for p in cm_client.ClouderaManagerResourceApi(self.api_client) - .update_config(message=self.message, body=body) - .items - ] + cm_api.update_config(message=self.message, body=body) self.config = [p.to_dict() for p in self.get_cm_config(scope=self.view)] else: @@ -225,7 +226,7 @@ def main(): argument_spec=dict( config=dict(type=dict, required=True, aliases=["parameters", "params"]), purge=dict(type="bool", default=False), - view=dict(choices=["summary", "full", "export"], default="summary"), + view=dict(choices=["summary", "full", "export"], default="full"), ), supports_check_mode=True, ) From d367b4a0184df765aeef068007c7016f873e4188 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 31 Jul 2025 09:32:55 -0400 Subject: [PATCH 7/9] Update cm_config tests to use latest pytest fixtures Signed-off-by: Webster Mudge --- .../modules/cm_config/test_cm_config.py | 66 ++++++------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/tests/unit/plugins/modules/cm_config/test_cm_config.py b/tests/unit/plugins/modules/cm_config/test_cm_config.py index 540db433..9a87b404 100644 --- a/tests/unit/plugins/modules/cm_config/test_cm_config.py +++ b/tests/unit/plugins/modules/cm_config/test_cm_config.py @@ -31,43 +31,20 @@ 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_ENDPOINT", None): - conn.update(url=os.getenv("CM_ENDPOINT")) - - if os.getenv("CM_PROXY", None): - conn.update(proxy=os.getenv("CM_PROXY")) - - return { - **conn, - "verify_tls": "no", - "debug": "no", - } - - def test_missing_parameters(conn, module_args): module_args(conn) - with pytest.raises(AnsibleFailJson, match="parameters"): + with pytest.raises(AnsibleFailJson, match="config"): cm_config.main() + + def test_set_config(conn, module_args): - conn.update( - parameters=dict(custom_header_color="PURPLE"), - # _ansible_check_mode=True, - # _ansible_diff=True, - ) - module_args(conn) + module_args({ + **conn, + "parameters": dict(custom_header_color="PURPLE"), + }) with pytest.raises(AnsibleExitJson) as e: cm_config.main() @@ -83,7 +60,10 @@ def test_set_config(conn, module_args): def test_unset_config(conn, module_args): - module_args({**conn, "parameters": dict(custom_header_color=None)}) + module_args({ + **conn, + "parameters": dict(custom_header_color=None) + }) with pytest.raises(AnsibleExitJson) as e: cm_config.main() @@ -99,13 +79,11 @@ def test_unset_config(conn, module_args): def test_set_config_with_purge(conn, module_args): - conn.update( - params=dict(custom_header_color="PURPLE"), - purge=True, - # _ansible_check_mode=True, - # _ansible_diff=True, - ) - module_args(conn) + module_args({ + **conn, + "params": dict(custom_header_color="PURPLE"), + "purge": True, + }) with pytest.raises(AnsibleExitJson) as e: cm_config.main() @@ -121,13 +99,11 @@ def test_set_config_with_purge(conn, module_args): def test_purge_all_config(conn, module_args): - conn.update( - params=dict(), - purge=True, - # _ansible_check_mode=True, - # _ansible_diff=True, - ) - module_args(conn) + module_args({ + **conn, + "params": dict(), + "purge": True, + }) with pytest.raises(AnsibleExitJson) as e: cm_config.main() From 99eb2ffe945a62c648361d76f69bb7181b784194 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 31 Jul 2025 09:33:13 -0400 Subject: [PATCH 8/9] Fix expected test results Signed-off-by: Webster Mudge --- tests/unit/plugins/modules/cm_service/test_cm_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/plugins/modules/cm_service/test_cm_service.py b/tests/unit/plugins/modules/cm_service/test_cm_service.py index 16bc7249..2b8aaba2 100644 --- a/tests/unit/plugins/modules/cm_service/test_cm_service.py +++ b/tests/unit/plugins/modules/cm_service/test_cm_service.py @@ -210,7 +210,7 @@ def test_new_config(conn, module_args, cms_cleared, request): }, ) - expected = dict(mgmt_emit_sensitive_data_in_stderr="True") + expected = dict(mgmt_emit_sensitive_data_in_stderr="true") with pytest.raises(AnsibleExitJson) as e: cm_service.main() @@ -291,7 +291,7 @@ def test_existing_set_parameters(conn, module_args, cms_config, request): ) expected = dict( - mgmt_emit_sensitive_data_in_stderr="True", + mgmt_emit_sensitive_data_in_stderr="true", log_event_retry_frequency="10", ) @@ -352,7 +352,7 @@ def test_existing_set_parameters_with_purge(conn, module_args, cms_config, reque }, ) - expected = dict(mgmt_emit_sensitive_data_in_stderr="True") + expected = dict(mgmt_emit_sensitive_data_in_stderr="true") with pytest.raises(AnsibleExitJson) as e: cm_service.main() From bc8a3219369c9f178e2ea89d453477059d8cae8c Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 31 Jul 2025 09:33:38 -0400 Subject: [PATCH 9/9] Add initial TESTING documentation Signed-off-by: Webster Mudge --- TESTING.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..b50a8dcb --- /dev/null +++ b/TESTING.md @@ -0,0 +1,31 @@ +# Testing + +_For details on how to write integration tests and a deep dive into the testing utilities of the collection, check out the [README within the `tests` directory](./tests/README.md)._ + +The collection uses the [pytest](https://pytest.org) to run a set of integration tests for the majority of the modules and plugins. The tests require a functioning _minimally_ configured deployment of Cloudera Manager and its agents. + +> [!IMPORANT] +> The Cloudera Manager Service nor a cluster are required for testing; the tests will construct the appropriate resources as needed. +> +> You must provide at minimum **three (3) servers** within the deployment. + +All (or most of) the tests require the following environment variables: + +- `CM_USERNAME` +- `CM_PASSWORD` +- `CDH_VERSION` + +And either: + +- `CM_HOST` +- `CM_PORT` + +Or: + +- `CM_ENDPOINT` (which is the full URL to the Cloudera Manager API endpoint) + +Optionally, + +- `CM_PROXY` + +Running the tests from the CLI is simply a `pytest` execution.