From be5771565babeb16eb46fd849b30c87a43dac53b Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:20 +0000 Subject: [PATCH 01/11] Initial commit of cm_kerberos module Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 399 ++++++++++++++++++ .../modules/cm_kerberos/test_cm_kerberos.py | 76 ++++ 2 files changed, 475 insertions(+) create mode 100644 plugins/modules/cm_kerberos.py create mode 100644 tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py new file mode 100644 index 00000000..9957e6a9 --- /dev/null +++ b/plugins/modules/cm_kerberos.py @@ -0,0 +1,399 @@ +#!/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 ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, + ClouderaManagerMutableModule, + resolve_parameter_updates, +) +from cm_client.rest import ApiException +from cm_client import ( + ClouderaManagerResourceApi, + ApiConfigList, + ApiConfig, +) + +DOCUMENTATION = r""" +module: cm_kerberos +short_description: Manage and configure Kerberos Authentication for CDP +description: + - Manages Kerberos authentication and configuration in Cloudera Manager. + - Imports the KDC Account Manager credentials needed by Cloudera Manager to create kerberos principals. +author: + - "Jim Enright (@jimright)" +requirements: + - cm_client +options: + state: + description: + - The declarative state of Kerberos configuration. + type: str + required: false + default: present + choices: + - present + - absent + krb_enc_types: + description: + - Kerberos Encryption Types supported by the KDC to set in Cloudera Manager configuration. + type: list + elements: str + required: false + security_realm: + description: + - Kerberos Security Realm to set in Cloudera Manager configuration + - Changing this setting would clear up all existing credentials and keytabs from Cloudera Manager. + type: str + required: false + kdc_type: + description: + - Type of KDC Kerberos key distribution center (KDC) used for authentication. + type: str + required: false + default: present + choices: + - 'MIT KDC' + - 'Active Directory' + - 'Red Hat IPA' + kdc_admin_host: + description: + - KDC Admin Server Host + - Port number is optional and can be provided as V(hostname:port) + type: str + required: false + kdc_host: + description: + - KDC Server Host + - Port number is optional and can be provided as V(hostname:port) + type: str + required: false + krb_auth_enable: + description: + - Enable SPNEGO/Kerberos Authentication for the Admin Console and API + type: bool + required: false + ad_account_prefix: + description: + - Prefix used in names while creating accounts in Active Directory. + - The prefix can be up to 15 characters long and can be set to identify accounts used for authentication by CDH processes. + - Used only if O(kdc_type='Active Directory'). + type: str + required: false + ad_kdc_domain: + description: + - Active Directory suffix where all the accounts used by CDH daemons will be created. + - Used only if O(kdc_type='Active Directory'). + type: str + required: false + ad_delete_on_regenerate: + description: + - Active Directory Delete Accounts on Credential Regeneration. + - Set this option to V(true) if regeneration of credentials should automatically delete the associated Active Directory accounts. + - Used only if O(kdc_type='Active Directory'). + type: bool + required: false + ad_set_encryption_types: + description: + - Set this V(true) if creation of Active Directory accounts should automatically turn on the associated encryption types represented by the msDS-EncryptionTypes field. + - Used only if O(kdc_type='Active Directory'). + type: bool + required: false + kdc_account_creation_host_override: + description: + - Active Directory Domain Controller host override. + - This parameter should be used when multiple Active Directory Domain Controllers are behind a load-balancer. + - This parameter should be set with the address of one of them AD Domain Controller. + - This setting is used only while creating accounts. CDH services use the value entered in the O(kdc_host) while authenticating. + - Only applicable if O(kdc_type='Active Directory') + type: str + required: false + gen_keytab_script: + description: + - Custom Kerberos Keytab Retrieval Script. + - Specify the path to a custom script, or executable, to retrieve a Kerberos keytab. + - The script should take two arguments - a destination file to write the keytab to, and the full principal name to retrieve the key for. + type: str + required: false + kdc_admin_user: + description: + - Username of the Kerberos Account Manager to create kerberos principals. + - The Kerberos realm must be specified in the principal name, for example V(username@CLDR.EXAMPLE). + type: str + required: false + kdc_admin_password: + description: + - Password of the Kerberos Account Manager to create kerberos principals. + type: str + required: false +extends_documentation_fragment: + - cloudera.cluster.cm_endpoint + - cloudera.cluster.message +notes: + - Using the C(cm_config) with O(purge=yes) will remove the Cloudera Manager configurations set by this module. + - Requires C(cm_client). +seealso: + - module: cloudera.cluster.cm_config +""" + +EXAMPLES = r""" +--- +- name: Enable Kerberos + cloudera.cluster.cm_kerberos: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + state: present + +- name: Disable Kerberos + cloudera.cluster.cm_autotls: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + state: absent +""" + +RETURN = r""" +--- +TODO: +""" + + +class ClouderaManagerKerberos(ClouderaManagerMutableModule): + def __init__(self, module): + super(ClouderaManagerKerberos, self).__init__(module) + + # Set the parameters + self.state = self.get_param("state") + self.krb_enc_types = self.get_param("krb_enc_types") + self.security_realm = self.get_param("security_realm") + self.kdc_type = self.get_param("kdc_type") + self.kdc_admin_host = self.get_param("kdc_admin_host") + self.kdc_host = self.get_param("kdc_host") + self.krb_auth_enable = self.get_param("krb_auth_enable") + self.ad_account_prefix = self.get_param("ad_account_prefix") + self.ad_kdc_domain = self.get_param("ad_kdc_domain") + self.ad_delete_on_regenerate = self.get_param("ad_delete_on_regenerate") + self.ad_set_encryption_types = self.get_param("ad_set_encryption_types") + self.kdc_account_creation_host_override = self.get_param( + "kdc_account_creation_host_override") + self.gen_keytab_script = self.get_param("gen_keytab_script") + self.kdc_admin_user = self.get_param("kdc_admin_user") + self.kdc_admin_password = self.get_param("kdc_admin_password") + + # Initialize the return values + self.output = {} + self.changed = False + self.diff = {} + + # Execute the logic + self.process() + + @ClouderaManagerMutableModule.handle_process + def process(self): + + # Check parameters that should only specified with skdc_type == Active Directory + if self.kdc_type != "Active Directory" and ( + self.ad_account_prefix + or self.ad_kdc_domain + or self.ad_delete_on_regenerate + or self.ad_set_encryption_types + ): + self.module.fail_json( + msg="Parameters 'ad_account_prefix', 'ad_kdc_domain', 'ad_delete_on_regenerate' or 'ad_set_encryption_types' can only be used with 'kdc_type = Active Directory'" + ) + + # Convert encryption types to space separated string + if self.krb_enc_types: + self.krb_enc_types = " ".join(self.krb_enc_types) + + # create an instance of the API class + cm_api_instance = ClouderaManagerResourceApi(self.api_client) + + # Check current CM configuration + existing = self.get_cm_config(scope="full") + current = {r.name: r.value for r in existing} + + # State present + if self.state == "present": + + # Determine CM configuration changes for Kerberos + incoming = { + key.upper(): getattr(self, key) + for key in [ + "krb_enc_types", + "security_realm", + "kdc_type", + "kdc_admin_host", + "kdc_host", + "krb_auth_enable", + "ad_account_prefix", + "ad_kdc_domain", + "ad_delete_on_regenerate", + "ad_set_encryption_types", + "kdc_account_creation_host_override", + "gen_keytab_script", + ] + } + change_set = resolve_parameter_updates(current, incoming) + + if change_set: + self.changed = True + + if self.module._diff: + self.diff = dict( + before={k: current[k] for k in change_set.keys()}, + after=change_set, + ) + + if not self.module.check_mode: + body = ApiConfigList( + items=[ApiConfig(name=k, value=v) for k, v in change_set.items()] + ) + config = cm_api_instance.update_config(message=self.message, body=body).items + + # Set output + self.output.update(cm_config=config) + + print(config) + + # Generate Kerberos credentials + # Check and create Kerberos credentials if required + if self.kdc_admin_user and self.kdc_admin_password: + # Check 1 - Retrieve CM Kerberos information + krb_info = cm_api_instance.get_kerberos_info().to_dict() + print(krb_info) + if krb_info.get("kerberized") == False: + print("Not kerberized") + + # TODO: Is there another check that we can do if we find kerberized false? + + # Generate credentials + if not self.module.check_mode: + admin_creds = cm_api_instance.import_admin_credentials(username=self.kdc_admin_user, password=self.kdc_admin_password) + + print(admin_creds) + + elif self.state == "absent": + + # Remove Kerberos credentials + if not self.module.check_mode: + krb_info = cm_api_instance.get_kerberos_info().to_dict() + if krb_info.get("kerberized") == True: + cm_api_instance.delete_credentials_command() + + # TODO: Review if this is best approach for absent + + # Reset CM configurations + # NOTE: 1 Attempt with list and value = None + reset_params = [ + "krb_enc_types", "security_realm", "kdc_type", + "kdc_admin_host", "kdc_host", "krb_auth_enable", + "ad_account_prefix", "ad_kdc_domain", "ad_delete_on_regenerate", + "ad_set_encryption_types", "kdc_account_creation_host_override", + "gen_keytab_script" + ] + + # if not self.module.check_mode: + # # body = ApiConfigList( + # # items=[ + # # ApiConfig(name=k, value=None) for k in reset_params + # # ] + # # ) + # cm_api_instance.update_config(body=body) + + # NOTE: 2 Attempt with dict + reset_params = dict( + krb_enc_types="aes256-cts", + security_realm="HADOOP.COM", + kdc_type="MIT KDC", + kdc_admin_host="", + kdc_host="", + krb_auth_enable=False, + ad_account_prefix="", + ad_kdc_domain="ou=hadoop,DC=hadoop,DC=com", + ad_delete_on_regenerate=False, + ad_set_encryption_types=False, + kdc_account_creation_host_override="", + gen_keytab_script="" + ) + # NOTE: Change set is always > 0 + change_set = resolve_parameter_updates(current, {k.upper():v for k,v in reset_params.items()}) + + if change_set: + self.changed = True + + if self.module._diff: + self.diff = dict( + before={k: current[k] for k in reset_params.keys()}, + after=reset_params, + ) + + if not self.module.check_mode: + body = ApiConfigList( + items=[ + ApiConfig(name=k, value=v) for k, v in reset_params.items() + ] + ) + config = cm_api_instance.update_config(body=body).items + + # Set output + self.output.update(cm_config=config) + +def main(): + + module = ClouderaManagerMutableModule.ansible_module( + argument_spec=dict( + krb_enc_types=dict(required=False, type="list"), + security_realm=dict(required=False, type="str"), + kdc_type=dict( + type="str", + choices=["MIT KDC", "Active Directory", "Red Hat IPA"], + ), + kdc_admin_host=dict(required=False, type="str"), + kdc_host=dict(required=False, type="str"), + krb_auth_enable=dict(required=False, type="bool"), + ad_account_prefix=dict(required=False, type="str"), + ad_kdc_domain=dict(required=False, type="str"), + ad_delete_on_regenerate=dict(required=False, type="bool"), + ad_set_encryption_types=dict(required=False, type="bool"), + kdc_account_creation_host_override=dict(required=False, type="str"), + gen_keytab_script=dict(required=False, type="str"), + kdc_admin_user=dict(required=False, type="str"), + kdc_admin_password=dict(required=False, type="str"), + state=dict(type="str", default="present", choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + result = ClouderaManagerKerberos(module) + + print(result) + output = dict( + changed=result.changed, + **result.output, + ) + if module._diff: + output.update(diff=result.diff) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py new file mode 100644 index 00000000..aaa8547d --- /dev/null +++ b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py @@ -0,0 +1,76 @@ +# -*- 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 os +import logging +import pytest + +from pathlib import Path + +from ansible_collections.cloudera.cluster.plugins.modules import cm_kerberos +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, + AnsibleFailJson, +) + +LOG = logging.getLogger(__name__) + +def test_pytest_enable_kerberos(module_args, conn, request): + + if os.getenv("KDC_ADMIN_USER", None): + conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) + + if os.getenv("KDC_ADMIN_PASSWORD", None): + conn.update(kdc_admin_password=os.getenv("KDC_ADMIN_PASSWORD")) + + if os.getenv("KDC_HOST", None): + conn.update(kdc_admin_host=os.getenv("KDC_HOST")) + conn.update(kdc_host=os.getenv("KDC_HOST")) + + module_args( + { + **conn, + "state": "present", + "kdc_type": "Red Hat IPA", + "krb_enc_types": ["aes256-cts", "aes128-cts", "rc4-hmac"], + "security_realm": "CLDR.INTERNAL", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + } + ) + + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos.main() + + # assert e.value.changed == True + + # LOG.info(str(e.value.user_output)) + +def test_pytest_disable_kerberos(module_args, conn, request): + + module_args( + { + **conn, + "state": "absent" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos.main() + + # assert e.value.changed == True From b5fd957677956314ca6d8934ab805e0916f47d45 Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:21 +0000 Subject: [PATCH 02/11] Fix idempotency issue and support error handling Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 46 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index 9957e6a9..6327c643 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -26,6 +26,7 @@ ApiConfigList, ApiConfig, ) +import re DOCUMENTATION = r""" module: cm_kerberos @@ -171,7 +172,6 @@ TODO: """ - class ClouderaManagerKerberos(ClouderaManagerMutableModule): def __init__(self, module): super(ClouderaManagerKerberos, self).__init__(module) @@ -199,6 +199,12 @@ def __init__(self, module): self.changed = False self.diff = {} + self.delay = 15 # Sleep time between wait for import_admin_credentials cmd to complete + # List of known acceptable errors in import_admin_credentials cmd + self.creds_known_errors = [ + r"ERROR: user with name.*already exists" + ] + # Execute the logic self.process() @@ -263,30 +269,34 @@ def process(self): body = ApiConfigList( items=[ApiConfig(name=k, value=v) for k, v in change_set.items()] ) - config = cm_api_instance.update_config(message=self.message, body=body).items - - # Set output - self.output.update(cm_config=config) - - print(config) + cm_api_instance.update_config(message=self.message, body=body).items # Generate Kerberos credentials # Check and create Kerberos credentials if required if self.kdc_admin_user and self.kdc_admin_password: # Check 1 - Retrieve CM Kerberos information krb_info = cm_api_instance.get_kerberos_info().to_dict() - print(krb_info) - if krb_info.get("kerberized") == False: - print("Not kerberized") - - # TODO: Is there another check that we can do if we find kerberized false? + if krb_info.get("kerberized") == False: + # Generate credentials if not self.module.check_mode: - admin_creds = cm_api_instance.import_admin_credentials(username=self.kdc_admin_user, password=self.kdc_admin_password) - - print(admin_creds) + cmd = cm_api_instance.import_admin_credentials(username=self.kdc_admin_user, password=self.kdc_admin_password) + creds_cmd_result = next(iter(self.wait_for_command_state(command_id=cmd.id, polling_interval=self.delay)),None) + + if creds_cmd_result.success: + self.changed = True + else: + # Check for known, acceptable errors in import_admin_credentials + if not any(re.search(item, creds_cmd_result.result_message) for item in self.creds_known_errors): + self.module.fail_json( + msg="Error during Import KDC Account Manager Credentials command", + error=creds_cmd_result.result_message, + ) + # Retrieve cm_config again after enabling Kerberos + self.output.update(cm_config = [r.to_dict() for r in self.get_cm_config()]) + elif self.state == "absent": # Remove Kerberos credentials @@ -348,10 +358,11 @@ def process(self): ApiConfig(name=k, value=v) for k, v in reset_params.items() ] ) - config = cm_api_instance.update_config(body=body).items + cm_api_instance.update_config(body=body).items # Set output - self.output.update(cm_config=config) + # Retrieve cm_config again after enabling Kerberos + self.output.update(cm_config = [r.to_dict() for r in self.get_cm_config()]) def main(): @@ -381,7 +392,6 @@ def main(): result = ClouderaManagerKerberos(module) - print(result) output = dict( changed=result.changed, **result.output, From c70d99975870fc91dfe0b654b35edf8f35c10382 Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:23 +0000 Subject: [PATCH 03/11] Update docs and add test Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 101 ++++++++++++++---- .../modules/cm_kerberos/test_cm_kerberos.py | 29 ++++- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index 6327c643..ebdf78f8 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -157,10 +157,17 @@ host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" + security_realm: "CLDR.INTERNAL" + kdc_type: "Red Hat IPA" + krb_enc_types: "aes256-cts aes128-cts rc4-hmac" + kdc_admin_host: "freeipa.cldr.internal" + kdc_host: "freeipa.cldr.internal" + kdc_admin_user: "admin@CLDR.INTERNAL" + kdc_admin_password: "kdcExamplePass" state: present - name: Disable Kerberos - cloudera.cluster.cm_autotls: + cloudera.cluster.cm_kerberos: host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" @@ -169,7 +176,75 @@ RETURN = r""" --- -TODO: +cm_config: + description: + - Cloudera Manager Server configurations with Kerberos settings where available. + type: list + elements: dict + returned: always + contains: + name: + description: + - The canonical name that identifies this configuration parameter. + type: str + returned: always + value: + description: + - The user-defined value. + - When absent, the default value (if any) will be used. + - Can also be absent, when enumerating allowed configs. + type: str + returned: when supported + required: + description: + - Whether this configuration is required for the object. + - If any required configuration is not set, operations on the object may not work. + type: bool + returned: when supported + default: + description: + - The default value. + type: str + returned: when supported + display_name: + description: + - A user-friendly name of the parameters, as would have been shown in the web UI. + type: str + returned: when supported + description: + description: + - A textual description of the parameter. + type: str + returned: when supported + related_name: + description: + - If applicable, contains the related configuration variable used by the source project. + type: str + returned: when supported + sensitive: + description: + - Whether this configuration is sensitive, i.e. contains information such as passwords. + - This parameter might affect how the value of this configuration might be shared by the caller. + type: bool + returned: when supported + validate_state: + description: + - State of the configuration parameter after validation. + - For example, C(OK), C(WARNING), and C(ERROR). + type: str + returned: when supported + validation_message: + description: + - A message explaining the parameter's validation state. + type: str + returned: when supported + validation_warnings_suppressed: + description: + - Whether validation warnings associated with this parameter are suppressed. + - In general, suppressed validation warnings are hidden in the Cloudera Manager UI. + - Configurations that do not produce warnings will not contain this field. + type: bool + returned: when supported """ class ClouderaManagerKerberos(ClouderaManagerMutableModule): @@ -304,28 +379,8 @@ def process(self): krb_info = cm_api_instance.get_kerberos_info().to_dict() if krb_info.get("kerberized") == True: cm_api_instance.delete_credentials_command() - - # TODO: Review if this is best approach for absent - + # Reset CM configurations - # NOTE: 1 Attempt with list and value = None - reset_params = [ - "krb_enc_types", "security_realm", "kdc_type", - "kdc_admin_host", "kdc_host", "krb_auth_enable", - "ad_account_prefix", "ad_kdc_domain", "ad_delete_on_regenerate", - "ad_set_encryption_types", "kdc_account_creation_host_override", - "gen_keytab_script" - ] - - # if not self.module.check_mode: - # # body = ApiConfigList( - # # items=[ - # # ApiConfig(name=k, value=None) for k in reset_params - # # ] - # # ) - # cm_api_instance.update_config(body=body) - - # NOTE: 2 Attempt with dict reset_params = dict( krb_enc_types="aes256-cts", security_realm="HADOOP.COM", diff --git a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py index aaa8547d..93e7c59a 100644 --- a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py +++ b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py @@ -57,11 +57,34 @@ def test_pytest_enable_kerberos(module_args, conn, request): with pytest.raises(AnsibleExitJson) as e: cm_kerberos.main() - # assert e.value.changed == True + assert e.value.changed == True + +def test_enable_invalid_admin_password(module_args, conn, request): + + if os.getenv("KDC_ADMIN_USER", None): + conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) - # LOG.info(str(e.value.user_output)) + if os.getenv("KDC_HOST", None): + conn.update(kdc_admin_host=os.getenv("KDC_HOST")) + conn.update(kdc_host=os.getenv("KDC_HOST")) + + module_args( + { + **conn, + "state": "present", + "kdc_type": "Red Hat IPA", + "krb_enc_types": ["aes256-cts", "aes128-cts", "rc4-hmac"], + "security_realm": "CLDR.INTERNAL", + "kdc_admin_password": "wrongPass", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + } + ) + + with pytest.raises(AnsibleFailJson, match="Error during Import KDC Account Manager Credentials command") as e: + cm_kerberos.main() + print("At end") -def test_pytest_disable_kerberos(module_args, conn, request): +def test_pytest_disable_kerberos(module_args, conn): module_args( { From a3e3770da6f4f448d654a58f12626ed95aca561d Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:23 +0000 Subject: [PATCH 04/11] Add cm_kerberos_info module Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos_info.py | 177 ++++++++++++++++++ .../cm_kerberos_info/test_cm_kerberos_info.py | 38 ++++ 2 files changed, 215 insertions(+) create mode 100644 plugins/modules/cm_kerberos_info.py create mode 100644 tests/unit/plugins/modules/cm_kerberos_info/test_cm_kerberos_info.py diff --git a/plugins/modules/cm_kerberos_info.py b/plugins/modules/cm_kerberos_info.py new file mode 100644 index 00000000..16cbecf1 --- /dev/null +++ b/plugins/modules/cm_kerberos_info.py @@ -0,0 +1,177 @@ +#!/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 ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) +from cm_client.rest import ApiException + +DOCUMENTATION = r""" +module: cm_kerberos_info +short_description: Retrieve Cloudera Manager configurations for Kerberos +description: + - Retrieve Cloudera Manager configurations for Kerberos +author: + - "Jim Enright (@jimright)" +requirements: + - cm_client +extends_documentation_fragment: + - ansible.builtin.action_common_attributes + - cloudera.cluster.cm_options + - cloudera.cluster.cm_endpoint +attributes: + check_mode: + support: full +notes: + - This is a convenience module to retrieve Kerberos settings from the Cloudera Manager configuration. + - Using the C(cm_config_info) module will return similar settings. + - Requires C(cm_client). +seealso: + - module: cloudera.cluster.cm_config_info +""" + +EXAMPLES = r""" +--- +- name: Retrieve CM Kerberos settings + cloudera.cluster.cm_kerberos_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + register: __kerberos_settings +""" + +RETURN = r""" +cm_config: + description: + - Cloudera Manager Server configurations with Kerberos settings where available. + type: list + elements: dict + returned: always + contains: + name: + description: + - The canonical name that identifies this configuration parameter. + type: str + returned: always + value: + description: + - The user-defined value. + - When absent, the default value (if any) will be used. + - Can also be absent, when enumerating allowed configs. + type: str + returned: when supported + required: + description: + - Whether this configuration is required for the object. + - If any required configuration is not set, operations on the object may not work. + type: bool + returned: when supported + default: + description: + - The default value. + type: str + returned: when supported + display_name: + description: + - A user-friendly name of the parameters, as would have been shown in the web UI. + type: str + returned: when supported + description: + description: + - A textual description of the parameter. + type: str + returned: when supported + related_name: + description: + - If applicable, contains the related configuration variable used by the source project. + type: str + returned: when supported + sensitive: + description: + - Whether this configuration is sensitive, i.e. contains information such as passwords. + - This parameter might affect how the value of this configuration might be shared by the caller. + type: bool + returned: when supported + validate_state: + description: + - State of the configuration parameter after validation. + - For example, C(OK), C(WARNING), and C(ERROR). + type: str + returned: when supported + validation_message: + description: + - A message explaining the parameter's validation state. + type: str + returned: when supported + validation_warnings_suppressed: + description: + - Whether validation warnings associated with this parameter are suppressed. + - In general, suppressed validation warnings are hidden in the Cloudera Manager UI. + - Configurations that do not produce warnings will not contain this field. + type: bool + returned: when supported +""" + + +class ClouderaManagerKerberosInfo(ClouderaManagerModule): + def __init__(self, module): + super(ClouderaManagerKerberosInfo, self).__init__(module) + + # Initialize the return values + self.config = [] + + # Execute the logic + self.process() + + @ClouderaManagerModule.handle_process + def process(self): + + kerberos_cm_settings = [ + "krb_enc_types", "security_realm", "kdc_type", + "kdc_admin_host", "kdc_host", "krb_auth_enable", + "ad_account_prefix", "ad_kdc_domain", "ad_delete_on_regenerate", + "ad_set_encryption_types", "kdc_account_creation_host_override", + "gen_keytab_script" + ] + + # Retrieve the cm configuration + cm_config = [r.to_dict() for r in self.get_cm_config(scope="full")] + self.config = [r for r in cm_config if r["name"].lower() in kerberos_cm_settings] + + +def main(): + module = ClouderaManagerModule.ansible_module( + argument_spec=dict(), + supports_check_mode=True, + ) + + result = ClouderaManagerKerberosInfo(module) + + output = dict( + changed=result.changed, + cm_config=result.config, + ) + + if result.debug: + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) + + module.exit_json(**output) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/cm_kerberos_info/test_cm_kerberos_info.py b/tests/unit/plugins/modules/cm_kerberos_info/test_cm_kerberos_info.py new file mode 100644 index 00000000..09753c9d --- /dev/null +++ b/tests/unit/plugins/modules/cm_kerberos_info/test_cm_kerberos_info.py @@ -0,0 +1,38 @@ +# -*- 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 ansible_collections.cloudera.cluster.plugins.modules import cm_kerberos_info +from ansible_collections.cloudera.cluster.tests.unit import ( + AnsibleExitJson, +) + +LOG = logging.getLogger(__name__) + + +def test_pytest_cm_kerberos_info(module_args, conn): + + module_args({**conn}) + + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos_info.main() + + assert len(e.value.cm_config) > 0 From 3f811e6c6f2c3cd47dbb7c7bf5e2c5b72aab5411 Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:27 +0000 Subject: [PATCH 05/11] Fix lint issues Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 219 ++++++++++-------- plugins/modules/cm_kerberos_info.py | 21 +- .../modules/cm_kerberos/test_cm_kerberos.py | 25 +- 3 files changed, 149 insertions(+), 116 deletions(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index ebdf78f8..edde2221 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# +# # Copyright 2025 Cloudera, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -90,7 +90,7 @@ ad_account_prefix: description: - Prefix used in names while creating accounts in Active Directory. - - The prefix can be up to 15 characters long and can be set to identify accounts used for authentication by CDH processes. + - The prefix can be up to 15 characters long and can be set to identify accounts used for authentication by CDH processes. - Used only if O(kdc_type='Active Directory'). type: str required: false @@ -103,20 +103,20 @@ ad_delete_on_regenerate: description: - Active Directory Delete Accounts on Credential Regeneration. - - Set this option to V(true) if regeneration of credentials should automatically delete the associated Active Directory accounts. + - Set this option to V(true) if regeneration of credentials should automatically delete the associated Active Directory accounts. - Used only if O(kdc_type='Active Directory'). type: bool required: false ad_set_encryption_types: description: - - Set this V(true) if creation of Active Directory accounts should automatically turn on the associated encryption types represented by the msDS-EncryptionTypes field. + - Set this V(true) if creation of Active Directory accounts should automatically turn on the associated encryption types represented by the msDS-EncryptionTypes field. - Used only if O(kdc_type='Active Directory'). type: bool required: false kdc_account_creation_host_override: description: - Active Directory Domain Controller host override. - - This parameter should be used when multiple Active Directory Domain Controllers are behind a load-balancer. + - This parameter should be used when multiple Active Directory Domain Controllers are behind a load-balancer. - This parameter should be set with the address of one of them AD Domain Controller. - This setting is used only while creating accounts. CDH services use the value entered in the O(kdc_host) while authenticating. - Only applicable if O(kdc_type='Active Directory') @@ -125,7 +125,7 @@ gen_keytab_script: description: - Custom Kerberos Keytab Retrieval Script. - - Specify the path to a custom script, or executable, to retrieve a Kerberos keytab. + - Specify the path to a custom script, or executable, to retrieve a Kerberos keytab. - The script should take two arguments - a destination file to write the keytab to, and the full principal name to retrieve the key for. type: str required: false @@ -247,6 +247,7 @@ returned: when supported """ + class ClouderaManagerKerberos(ClouderaManagerMutableModule): def __init__(self, module): super(ClouderaManagerKerberos, self).__init__(module) @@ -264,7 +265,8 @@ def __init__(self, module): self.ad_delete_on_regenerate = self.get_param("ad_delete_on_regenerate") self.ad_set_encryption_types = self.get_param("ad_set_encryption_types") self.kdc_account_creation_host_override = self.get_param( - "kdc_account_creation_host_override") + "kdc_account_creation_host_override" + ) self.gen_keytab_script = self.get_param("gen_keytab_script") self.kdc_admin_user = self.get_param("kdc_admin_user") self.kdc_admin_password = self.get_param("kdc_admin_password") @@ -274,11 +276,11 @@ def __init__(self, module): self.changed = False self.diff = {} - self.delay = 15 # Sleep time between wait for import_admin_credentials cmd to complete + self.delay = ( + 15 # Sleep time between wait for import_admin_credentials cmd to complete + ) # List of known acceptable errors in import_admin_credentials cmd - self.creds_known_errors = [ - r"ERROR: user with name.*already exists" - ] + self.creds_known_errors = [r"ERROR: user with name.*already exists"] # Execute the logic self.process() @@ -307,81 +309,96 @@ def process(self): # Check current CM configuration existing = self.get_cm_config(scope="full") current = {r.name: r.value for r in existing} - + # State present if self.state == "present": - # Determine CM configuration changes for Kerberos - incoming = { - key.upper(): getattr(self, key) - for key in [ - "krb_enc_types", - "security_realm", - "kdc_type", - "kdc_admin_host", - "kdc_host", - "krb_auth_enable", - "ad_account_prefix", - "ad_kdc_domain", - "ad_delete_on_regenerate", - "ad_set_encryption_types", - "kdc_account_creation_host_override", - "gen_keytab_script", - ] - } - change_set = resolve_parameter_updates(current, incoming) - - if change_set: - self.changed = True - - if self.module._diff: - self.diff = dict( - before={k: current[k] for k in change_set.keys()}, - after=change_set, - ) - - if not self.module.check_mode: - body = ApiConfigList( - items=[ApiConfig(name=k, value=v) for k, v in change_set.items()] - ) - cm_api_instance.update_config(message=self.message, body=body).items - - # Generate Kerberos credentials - # Check and create Kerberos credentials if required - if self.kdc_admin_user and self.kdc_admin_password: - # Check 1 - Retrieve CM Kerberos information - krb_info = cm_api_instance.get_kerberos_info().to_dict() - - if krb_info.get("kerberized") == False: - - # Generate credentials - if not self.module.check_mode: - cmd = cm_api_instance.import_admin_credentials(username=self.kdc_admin_user, password=self.kdc_admin_password) - creds_cmd_result = next(iter(self.wait_for_command_state(command_id=cmd.id, polling_interval=self.delay)),None) - - if creds_cmd_result.success: - self.changed = True - else: - # Check for known, acceptable errors in import_admin_credentials - if not any(re.search(item, creds_cmd_result.result_message) for item in self.creds_known_errors): - self.module.fail_json( - msg="Error during Import KDC Account Manager Credentials command", - error=creds_cmd_result.result_message, + # Determine CM configuration changes for Kerberos + incoming = { + key.upper(): getattr(self, key) + for key in [ + "krb_enc_types", + "security_realm", + "kdc_type", + "kdc_admin_host", + "kdc_host", + "krb_auth_enable", + "ad_account_prefix", + "ad_kdc_domain", + "ad_delete_on_regenerate", + "ad_set_encryption_types", + "kdc_account_creation_host_override", + "gen_keytab_script", + ] + } + change_set = resolve_parameter_updates(current, incoming) + + if change_set: + self.changed = True + + if self.module._diff: + self.diff = dict( + before={k: current[k] for k in change_set.keys()}, + after=change_set, + ) + + if not self.module.check_mode: + body = ApiConfigList( + items=[ + ApiConfig(name=k, value=v) for k, v in change_set.items() + ] ) + cm_api_instance.update_config(message=self.message, body=body).items + + # Generate Kerberos credentials + # Check and create Kerberos credentials if required + if self.kdc_admin_user and self.kdc_admin_password: + # Check 1 - Retrieve CM Kerberos information + krb_info = cm_api_instance.get_kerberos_info().to_dict() + + if krb_info.get("kerberized") == False: + + # Generate credentials + if not self.module.check_mode: + cmd = cm_api_instance.import_admin_credentials( + username=self.kdc_admin_user, + password=self.kdc_admin_password, + ) + creds_cmd_result = next( + iter( + self.wait_for_command_state( + command_id=cmd.id, polling_interval=self.delay + ) + ), + None, + ) + + if creds_cmd_result.success: + self.changed = True + else: + # Check for known, acceptable errors in import_admin_credentials + if not any( + re.search(item, creds_cmd_result.result_message) + for item in self.creds_known_errors + ): + self.module.fail_json( + msg="Error during Import KDC Account Manager Credentials command", + error=creds_cmd_result.result_message, + ) + + # Retrieve cm_config again after enabling Kerberos + self.output.update(cm_config=[r.to_dict() for r in self.get_cm_config()]) - # Retrieve cm_config again after enabling Kerberos - self.output.update(cm_config = [r.to_dict() for r in self.get_cm_config()]) - elif self.state == "absent": - # Remove Kerberos credentials - if not self.module.check_mode: - krb_info = cm_api_instance.get_kerberos_info().to_dict() - if krb_info.get("kerberized") == True: - cm_api_instance.delete_credentials_command() - - # Reset CM configurations - reset_params = dict( + # Remove Kerberos credentials + if not self.module.check_mode: + krb_info = cm_api_instance.get_kerberos_info().to_dict() + if krb_info.get("kerberized") == True: + cm_api_instance.delete_credentials_command() + + # Reset CM configurations + reset_params = dict( krb_enc_types="aes256-cts", security_realm="HADOOP.COM", kdc_type="MIT KDC", @@ -393,31 +410,36 @@ def process(self): ad_delete_on_regenerate=False, ad_set_encryption_types=False, kdc_account_creation_host_override="", - gen_keytab_script="" + gen_keytab_script="", ) - # NOTE: Change set is always > 0 - change_set = resolve_parameter_updates(current, {k.upper():v for k,v in reset_params.items()}) - - if change_set: - self.changed = True - - if self.module._diff: - self.diff = dict( - before={k: current[k] for k in reset_params.keys()}, - after=reset_params, - ) - - if not self.module.check_mode: - body = ApiConfigList( + # NOTE: Change set is always > 0 + change_set = resolve_parameter_updates( + current, {k.upper(): v for k, v in reset_params.items()} + ) + + if change_set: + self.changed = True + + if self.module._diff: + self.diff = dict( + before={k: current[k] for k in reset_params.keys()}, + after=reset_params, + ) + + if not self.module.check_mode: + body = ApiConfigList( items=[ ApiConfig(name=k, value=v) for k, v in reset_params.items() ] ) - cm_api_instance.update_config(body=body).items + cm_api_instance.update_config(body=body).items + + # Set output + # Retrieve cm_config again after enabling Kerberos + self.output.update( + cm_config=[r.to_dict() for r in self.get_cm_config()] + ) - # Set output - # Retrieve cm_config again after enabling Kerberos - self.output.update(cm_config = [r.to_dict() for r in self.get_cm_config()]) def main(): @@ -460,5 +482,6 @@ def main(): module.exit_json(**output) + if __name__ == "__main__": main() diff --git a/plugins/modules/cm_kerberos_info.py b/plugins/modules/cm_kerberos_info.py index 16cbecf1..8d7c21ce 100644 --- a/plugins/modules/cm_kerberos_info.py +++ b/plugins/modules/cm_kerberos_info.py @@ -141,16 +141,25 @@ def __init__(self, module): def process(self): kerberos_cm_settings = [ - "krb_enc_types", "security_realm", "kdc_type", - "kdc_admin_host", "kdc_host", "krb_auth_enable", - "ad_account_prefix", "ad_kdc_domain", "ad_delete_on_regenerate", - "ad_set_encryption_types", "kdc_account_creation_host_override", - "gen_keytab_script" + "krb_enc_types", + "security_realm", + "kdc_type", + "kdc_admin_host", + "kdc_host", + "krb_auth_enable", + "ad_account_prefix", + "ad_kdc_domain", + "ad_delete_on_regenerate", + "ad_set_encryption_types", + "kdc_account_creation_host_override", + "gen_keytab_script", ] # Retrieve the cm configuration cm_config = [r.to_dict() for r in self.get_cm_config(scope="full")] - self.config = [r for r in cm_config if r["name"].lower() in kerberos_cm_settings] + self.config = [ + r for r in cm_config if r["name"].lower() in kerberos_cm_settings + ] def main(): diff --git a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py index 93e7c59a..a197d788 100644 --- a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py +++ b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py @@ -31,8 +31,9 @@ LOG = logging.getLogger(__name__) + def test_pytest_enable_kerberos(module_args, conn, request): - + if os.getenv("KDC_ADMIN_USER", None): conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) @@ -53,12 +54,13 @@ def test_pytest_enable_kerberos(module_args, conn, request): "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", } ) - + with pytest.raises(AnsibleExitJson) as e: cm_kerberos.main() assert e.value.changed == True + def test_enable_invalid_admin_password(module_args, conn, request): if os.getenv("KDC_ADMIN_USER", None): @@ -79,20 +81,19 @@ def test_enable_invalid_admin_password(module_args, conn, request): "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", } ) - - with pytest.raises(AnsibleFailJson, match="Error during Import KDC Account Manager Credentials command") as e: + + with pytest.raises( + AnsibleFailJson, + match="Error during Import KDC Account Manager Credentials command", + ) as e: cm_kerberos.main() print("At end") + def test_pytest_disable_kerberos(module_args, conn): - - module_args( - { - **conn, - "state": "absent" - } - ) - + + module_args({**conn, "state": "absent"}) + with pytest.raises(AnsibleExitJson) as e: cm_kerberos.main() From 267e7cb6759273366b0c897745b6642d39d15cb4 Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:28 +0000 Subject: [PATCH 06/11] Update descriptions for module parameters Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index edde2221..de007a16 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -65,7 +65,6 @@ - Type of KDC Kerberos key distribution center (KDC) used for authentication. type: str required: false - default: present choices: - 'MIT KDC' - 'Active Directory' @@ -126,7 +125,7 @@ description: - Custom Kerberos Keytab Retrieval Script. - Specify the path to a custom script, or executable, to retrieve a Kerberos keytab. - - The script should take two arguments - a destination file to write the keytab to, and the full principal name to retrieve the key for. + - The target script should accept two arguments: a destination path for the resulting keytab and the full principal name of the owner of the keytab. type: str required: false kdc_admin_user: @@ -144,7 +143,7 @@ - cloudera.cluster.cm_endpoint - cloudera.cluster.message notes: - - Using the C(cm_config) with O(purge=yes) will remove the Cloudera Manager configurations set by this module. + - Using the C(cm_config) module with O(purge=yes) will remove the Cloudera Manager configurations set by this module. - Requires C(cm_client). seealso: - module: cloudera.cluster.cm_config From b861354bbb545a8fab17479596a36442a9541963 Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:30 +0000 Subject: [PATCH 07/11] Add force parameter to kerberos module Signed-off-by: Jim Enright --- .../modules/cm_kerberos/test_cm_kerberos.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py index a197d788..154aaad4 100644 --- a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py +++ b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py @@ -98,3 +98,42 @@ def test_pytest_disable_kerberos(module_args, conn): cm_kerberos.main() # assert e.value.changed == True + +def test_force_enable_kerberos(module_args, conn, request): + + if os.getenv("KDC_ADMIN_USER", None): + conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) + + if os.getenv("KDC_ADMIN_PASSWORD", None): + conn.update(kdc_admin_password=os.getenv("KDC_ADMIN_PASSWORD")) + + if os.getenv("KDC_HOST", None): + conn.update(kdc_admin_host=os.getenv("KDC_HOST")) + conn.update(kdc_host=os.getenv("KDC_HOST")) + + # Ensure Kerberos is enabled + module_args( + { + **conn, + "kdc_type": "Red Hat IPA", + "krb_enc_types": ["aes256-cts", "aes128-cts", "rc4-hmac"], + "security_realm": "CLDR.INTERNAL" + } + ) + + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos.main() + + # Add force to module call + module_args( + { + **conn, + "force": True, + "kdc_type": "Red Hat IPA", + "krb_enc_types": ["aes256-cts", "aes128-cts", "rc4-hmac"], + "security_realm": "CLDR.INTERNAL", + "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", + } + ) + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos.main() From d5daa5091292d5bd7a58eb3f59653e69cd9e23ee Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:32 +0000 Subject: [PATCH 08/11] Add force parameter to kerberos module Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index de007a16..f6daab30 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -48,6 +48,10 @@ choices: - present - absent + force: + description: + - Forces an attempt to generate the KDC Account Manager credentials even if Kerberos is already determined to be enabled. + - Applicable only when O(state) is V(present). krb_enc_types: description: - Kerberos Encryption Types supported by the KDC to set in Cloudera Manager configuration. @@ -57,7 +61,7 @@ security_realm: description: - Kerberos Security Realm to set in Cloudera Manager configuration - - Changing this setting would clear up all existing credentials and keytabs from Cloudera Manager. + - Changing this variable removes existing credentials and keytabs from Cloudera Manager and will attempt to re-generate these credentials. type: str required: false kdc_type: @@ -253,6 +257,7 @@ def __init__(self, module): # Set the parameters self.state = self.get_param("state") + self.force = self.get_param("force") self.krb_enc_types = self.get_param("krb_enc_types") self.security_realm = self.get_param("security_realm") self.kdc_type = self.get_param("kdc_type") @@ -352,10 +357,10 @@ def process(self): # Generate Kerberos credentials # Check and create Kerberos credentials if required if self.kdc_admin_user and self.kdc_admin_password: - # Check 1 - Retrieve CM Kerberos information + # Retrieve CM Kerberos information krb_info = cm_api_instance.get_kerberos_info().to_dict() - if krb_info.get("kerberized") == False: + if krb_info.get("kerberized") == False or self.force: # Generate credentials if not self.module.check_mode: @@ -462,6 +467,7 @@ def main(): kdc_admin_user=dict(required=False, type="str"), kdc_admin_password=dict(required=False, type="str"), state=dict(type="str", default="present", choices=["present", "absent"]), + force=dict(required=False, type="bool", default=False) ), supports_check_mode=True, ) From 19eaf19a6ec0fa7ce29b9ac0f94f28a35fa7458f Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:33 +0000 Subject: [PATCH 09/11] Fix lint issues Signed-off-by: Jim Enright --- plugins/modules/cm_kerberos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index f6daab30..416583dc 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -467,7 +467,7 @@ def main(): kdc_admin_user=dict(required=False, type="str"), kdc_admin_password=dict(required=False, type="str"), state=dict(type="str", default="present", choices=["present", "absent"]), - force=dict(required=False, type="bool", default=False) + force=dict(required=False, type="bool", default=False), ), supports_check_mode=True, ) From 73830fae53d3e267a5a6afd5d2bcf6f748c108a9 Mon Sep 17 00:00:00 2001 From: Jim Enright Date: Thu, 20 Mar 2025 20:52:34 +0000 Subject: [PATCH 10/11] Fix lint issues Signed-off-by: Jim Enright --- tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py index 154aaad4..fe1c9de5 100644 --- a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py +++ b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py @@ -99,6 +99,7 @@ def test_pytest_disable_kerberos(module_args, conn): # assert e.value.changed == True + def test_force_enable_kerberos(module_args, conn, request): if os.getenv("KDC_ADMIN_USER", None): @@ -117,7 +118,7 @@ def test_force_enable_kerberos(module_args, conn, request): **conn, "kdc_type": "Red Hat IPA", "krb_enc_types": ["aes256-cts", "aes128-cts", "rc4-hmac"], - "security_realm": "CLDR.INTERNAL" + "security_realm": "CLDR.INTERNAL", } ) From 6039b0bcffa399bf1dcceeced563ad73b46fdd35 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 20 Mar 2025 20:52:36 +0000 Subject: [PATCH 11/11] Add fixtures for Kerberos enable/disable for test preparation (#1) Add idempotence to tests Signed-off-by: Webster Mudge --- plugins/modules/cm_kerberos.py | 28 ++-- tests/unit/conftest.py | 8 +- .../modules/cm_kerberos/test_cm_kerberos.py | 137 +++++++++++++++--- 3 files changed, 133 insertions(+), 40 deletions(-) diff --git a/plugins/modules/cm_kerberos.py b/plugins/modules/cm_kerberos.py index 416583dc..65899ee8 100644 --- a/plugins/modules/cm_kerberos.py +++ b/plugins/modules/cm_kerberos.py @@ -15,19 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( - ClouderaManagerModule, - ClouderaManagerMutableModule, - resolve_parameter_updates, -) -from cm_client.rest import ApiException -from cm_client import ( - ClouderaManagerResourceApi, - ApiConfigList, - ApiConfig, -) -import re - DOCUMENTATION = r""" module: cm_kerberos short_description: Manage and configure Kerberos Authentication for CDP @@ -154,7 +141,6 @@ """ EXAMPLES = r""" ---- - name: Enable Kerberos cloudera.cluster.cm_kerberos: host: example.cloudera.com @@ -178,7 +164,6 @@ """ RETURN = r""" ---- cm_config: description: - Cloudera Manager Server configurations with Kerberos settings where available. @@ -250,6 +235,19 @@ returned: when supported """ +import re + +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerMutableModule, + resolve_parameter_updates, +) + +from cm_client import ( + ClouderaManagerResourceApi, + ApiConfigList, + ApiConfig, +) + class ClouderaManagerKerberos(ClouderaManagerMutableModule): def __init__(self, module): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 51973852..1831c044 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -26,7 +26,7 @@ import sys import yaml -from collections.abc import Generator +from collections.abc import Generator, Callable from pathlib import Path from time import sleep @@ -128,7 +128,7 @@ def fail_json(*args, **kwargs): @pytest.fixture -def module_args(): +def module_args() -> Callable[[dict], None]: """Prepare module arguments""" def prep_args(args=dict()): @@ -139,7 +139,7 @@ def prep_args(args=dict()): @pytest.fixture -def yaml_args(): +def yaml_args() -> Callable[[dict], None]: """Prepare module arguments from YAML""" def prep_args(args: str = ""): @@ -150,7 +150,7 @@ def prep_args(args: str = ""): @pytest.fixture(scope="session") -def conn(): +def conn() -> dict: conn = dict(username=os.getenv("CM_USERNAME"), password=os.getenv("CM_PASSWORD")) if os.getenv("CM_HOST", None): diff --git a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py index fe1c9de5..09104a9e 100644 --- a/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py +++ b/tests/unit/plugins/modules/cm_kerberos/test_cm_kerberos.py @@ -16,23 +16,118 @@ from __future__ import absolute_import, division, print_function +from cm_client.api_client import ApiClient + __metaclass__ = type + import os import logging import pytest +import re from pathlib import Path +from cm_client.rest import ApiException +from cm_client import ( + ClouderaManagerResourceApi, + ApiConfigList, + ApiConfig, +) + + from ansible_collections.cloudera.cluster.plugins.modules import cm_kerberos from ansible_collections.cloudera.cluster.tests.unit import ( AnsibleExitJson, AnsibleFailJson, + wait_for_command, ) LOG = logging.getLogger(__name__) -def test_pytest_enable_kerberos(module_args, conn, request): +@pytest.fixture(scope="function") +def krb_disabled(cm_api_client, request) -> None: + """ + Disable any existing Kerberos setup on the target Cloudera on Premise deployment. + + This fixture does not restore any prior configurations. + """ + + cm_api = ClouderaManagerResourceApi(cm_api_client) + + cm_api.delete_credentials_command() + + reset_params = dict( + krb_enc_types="aes256-cts", + security_realm="HADOOP.COM", + kdc_type="MIT KDC", + kdc_admin_host="", + kdc_host="", + krb_auth_enable=False, + ad_account_prefix="", + ad_kdc_domain="ou=hadoop,DC=hadoop,DC=com", + ad_delete_on_regenerate=False, + ad_set_encryption_types=False, + kdc_account_creation_host_override="", + gen_keytab_script="", + ) + + body = ApiConfigList( + items=[ApiConfig(name=k, value=v) for k, v in reset_params.items()] + ) + + cm_api.update_config( + message=f"{Path(request.node.parent.name).stem}::{request.node.name}::cleared", + body=body, + ) + + +# TODO Should parameterize with a marker +@pytest.fixture(scope="function") +def krb_freeipa(cm_api_client, request, krb_disabled) -> None: + """ + Reset any existing Kerberos setup on the target Cloudera on Premise deployment. + + This fixture does not restore any prior configurations. + """ + + cm_api = ClouderaManagerResourceApi(cm_api_client) + + setup_params = dict( + krb_enc_types="aes256-cts aes128-cts rc4-hmac", + security_realm="HADOOP.COM", + kdc_type="Red Hat IPA", + kdc_admin_host=os.getenv("KDC_HOST"), + kdc_host=os.getenv("KDC_HOST"), + ) + + body = ApiConfigList( + items=[ApiConfig(name=k, value=v) for k, v in setup_params.items()] + ) + + cm_api.update_config( + message=f"{Path(request.node.parent.name).stem}::{request.node.name}::enabled", + body=body, + ) + + cmd = cm_api.import_admin_credentials( + username=os.getenv("KDC_ADMIN_USER"), + password=os.getenv("KDC_ADMIN_PASSWORD"), + ) + + try: + wait_for_command( + api_client=cm_api_client, + command=cmd, + ) + except Exception as e: + if re.search("user with name", str(e)): + LOG.info("Reusing existing KDC user for Cloudera Manager") + else: + raise e + + +def test_pytest_enable_kerberos(module_args, conn, krb_disabled, request): if os.getenv("KDC_ADMIN_USER", None): conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) @@ -60,8 +155,14 @@ def test_pytest_enable_kerberos(module_args, conn, request): assert e.value.changed == True + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos.main() -def test_enable_invalid_admin_password(module_args, conn, request): + assert e.value.changed == False + + +def test_enable_invalid_admin_password(module_args, conn, krb_disabled, request): if os.getenv("KDC_ADMIN_USER", None): conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) @@ -85,22 +186,27 @@ def test_enable_invalid_admin_password(module_args, conn, request): with pytest.raises( AnsibleFailJson, match="Error during Import KDC Account Manager Credentials command", - ) as e: + ): cm_kerberos.main() - print("At end") -def test_pytest_disable_kerberos(module_args, conn): +def test_pytest_disable_kerberos(module_args, conn, krb_freeipa): module_args({**conn, "state": "absent"}) with pytest.raises(AnsibleExitJson) as e: cm_kerberos.main() - # assert e.value.changed == True + assert e.value.changed == True + # Idempotency + with pytest.raises(AnsibleExitJson) as e: + cm_kerberos.main() + + assert e.value.changed == False -def test_force_enable_kerberos(module_args, conn, request): + +def test_force_enable_kerberos(module_args, conn, krb_freeipa, request): if os.getenv("KDC_ADMIN_USER", None): conn.update(kdc_admin_user=os.getenv("KDC_ADMIN_USER")) @@ -112,20 +218,6 @@ def test_force_enable_kerberos(module_args, conn, request): conn.update(kdc_admin_host=os.getenv("KDC_HOST")) conn.update(kdc_host=os.getenv("KDC_HOST")) - # Ensure Kerberos is enabled - module_args( - { - **conn, - "kdc_type": "Red Hat IPA", - "krb_enc_types": ["aes256-cts", "aes128-cts", "rc4-hmac"], - "security_realm": "CLDR.INTERNAL", - } - ) - - with pytest.raises(AnsibleExitJson) as e: - cm_kerberos.main() - - # Add force to module call module_args( { **conn, @@ -136,5 +228,8 @@ def test_force_enable_kerberos(module_args, conn, request): "message": f"{Path(request.node.parent.name).stem}::{request.node.name}", } ) + with pytest.raises(AnsibleExitJson) as e: cm_kerberos.main() + + assert e.value.changed == True