From 621a98fe2f35b0001bc98adefcbb97ffeae83bf7 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 30 Nov 2023 10:51:29 -0500 Subject: [PATCH 1/3] Update logging and error handling for consistent debugging experience Add ssl_ca_cert parameter for custom SSL certificate validation. Update return field for cm_version_info. Signed-off-by: Webster Mudge --- plugins/doc_fragments/cm_options.py | 13 ++- plugins/module_utils/cm_utils.py | 174 +++++++++++++++------------- plugins/modules/cm_endpoint_info.py | 46 ++++---- plugins/modules/cm_resource.py | 82 +++++++------ plugins/modules/cm_resource_info.py | 77 ++++++------ plugins/modules/cm_version_info.py | 58 +++++----- 6 files changed, 240 insertions(+), 210 deletions(-) diff --git a/plugins/doc_fragments/cm_options.py b/plugins/doc_fragments/cm_options.py index 5aa13223..2a531e0d 100644 --- a/plugins/doc_fragments/cm_options.py +++ b/plugins/doc_fragments/cm_options.py @@ -15,8 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. + class ModuleDocFragment(object): - DOCUMENTATION = r''' + DOCUMENTATION = r""" options: host: description: @@ -55,6 +56,14 @@ class ModuleDocFragment(object): type: bool required: False default: True + ssl_ca_cert: + description: + - Path to SSL CA certificate to use for validation. + type: path + required: False + aliases: + - tls_cert + - ssl_cert username: description: - Username for access to the CM API endpoint. @@ -80,4 +89,4 @@ class ModuleDocFragment(object): type: str required: False default: ClouderaFoundry - ''' + """ diff --git a/plugins/module_utils/cm_utils.py b/plugins/module_utils/cm_utils.py index 6cf00c8e..3818c0aa 100644 --- a/plugins/module_utils/cm_utils.py +++ b/plugins/module_utils/cm_utils.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # Copyright 2023 Cloudera, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +27,7 @@ from urllib.parse import urljoin from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text from cm_client import ApiClient, Configuration from cm_client.rest import ApiException, RESTClientObject @@ -39,57 +37,65 @@ __credits__ = ["frisch@cloudera.com"] __maintainer__ = ["wmudge@cloudera.com"] -""" -A common Ansible Module for API access to Cloudera Manager. -""" class ClouderaManagerModule(object): + """Base Ansible Module for API access to Cloudera Manager.""" + @classmethod def handle_process(cls, f): - """Wrapper to handle log capture and common HTTP errors""" + """Wrapper to handle API, retry, and HTTP errors.""" @wraps(f) def _impl(self, *args, **kwargs): - try: - self._initialize_client() - result = f(self, *args, **kwargs) + def _add_log(err): if self.debug: - self.log_out = self._get_log() - self.log_lines.append(self.log_out.splitlines()) - return result + log = self.log_capture.getvalue() + err.update(debug=log, debug_lines=log.split("\n")) + return err + + try: + self.initialize_client() + return f(self, *args, **kwargs) except ApiException as ae: - body = ae.body.decode("utf-8") - if body != "": - body = json.loads(body) - self.module.fail_json( - msg="API error: " + str(ae.reason), status_code=ae.status, body=body + err = dict( + msg="API error: " + to_text(ae.reason), + status_code=ae.status, + body=ae.body.decode("utf-8"), ) + if err["body"] != "": + try: + err.update(body=json.loads(err["body"])) + except Exception as te: + pass + + self.module.fail_json(**_add_log(err)) except MaxRetryError as maxe: - self.module.fail_json(msg="Request error: " + str(maxe.reason)) + err = dict( + msg="Request error: " + to_text(maxe.reason), url=to_text(maxe.url) + ) + self.module.fail_json(**_add_log(err)) except HTTPError as he: - self.module.fail_json(msg="HTTP request: " + str(he)) + err = dict(msg="HTTP request: " + str(he)) + self.module.fail_json(**_add_log(err)) return _impl - """A base Cloudera Manager (CM) module class""" - def __init__(self, module): # Set common parameters self.module = module - self.url = self._get_param("url", None) - self.force_tls = self._get_param("force_tls") - self.host = self._get_param("host") - self.port = self._get_param("port") - self.version = self._get_param("version") - self.username = self._get_param("username") - self.password = self._get_param("password") - self.verify_tls = self._get_param("verify_tls") - self.debug = self._get_param("debug") - self.agent_header = self._get_param("agent_header") + self.url = self.get_param("url", None) + self.force_tls = self.get_param("force_tls") + self.host = self.get_param("host") + self.port = self.get_param("port") + self.version = self.get_param("version") + self.username = self.get_param("username") + self.password = self.get_param("password") + self.verify_tls = self.get_param("verify_tls") + self.ssl_ca_cert = self.get_param("ssl_ca_cert") + self.debug = self.get_param("debug") + self.agent_header = self.get_param("agent_header") # Initialize common return values - self.log_out = None - self.log_lines = [] self.changed = False # Configure the core CM API client parameters @@ -99,47 +105,46 @@ def __init__(self, module): config.verify_ssl = self.verify_tls config.debug = self.debug - # Configure logging - _log_format = ( + # Configure custom validation certificate + if self.ssl_ca_cert: + config.ssl_ca_cert = self.ssl_ca_cert + + # Create a common logging format + log_format = ( "%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s" ) + + # Configure the urllib3 logger + self.logger = logging.getLogger("urllib3") + if self.debug: - self._setup_logger(logging.DEBUG, _log_format) - self.logger.debug("CM API agent: %s", self.agent_header) - else: - self._setup_logger(logging.ERROR, _log_format) + self.logger.setLevel(logging.DEBUG) + + self.log_capture = io.StringIO() + handler = logging.StreamHandler(self.log_capture) + handler.setLevel(logging.DEBUG) + + formatter = logging.Formatter(log_format) + handler.setFormatter(formatter) + + self.logger.addHandler(handler) + + self.logger.debug("CM API agent: %s", self.agent_header) if self.verify_tls is False: disable_warnings(InsecureRequestWarning) - def _get_param(self, param, default=None): - """Fetches an Ansible input parameter if it exists, else returns optional default or None""" + def get_param(self, param, default=None): + """ + Fetches an Ansible input parameter if it exists, else returns optional + default or None. + """ if self.module is not None: return self.module.params[param] if param in self.module.params else default return default - def _setup_logger(self, log_level, log_format): - """Configures the logging of the HTTP activity""" - self.logger = logging.getLogger("urllib3") - self.logger.setLevel(log_level) - - self.__log_capture = io.StringIO() - handler = logging.StreamHandler(self.__log_capture) - handler.setLevel(log_level) - - formatter = logging.Formatter(log_format) - handler.setFormatter(formatter) - - self.logger.addHandler(handler) - - def _get_log(self): - """Retrieves the contents of the captured log""" - contents = self.__log_capture.getvalue() - self.__log_capture.truncate(0) - return contents - - def _initialize_client(self): - """Configures and creates the API client""" + def initialize_client(self): + """Creates the CM API client""" config = Configuration() # If provided a CML endpoint URL, use it directly @@ -147,21 +152,23 @@ def _initialize_client(self): config.host = self.url # Otherwise, run discovery on missing parts else: - config.host = self._discover_endpoint(config) + config.host = self.discover_endpoint(config) # Create and set the API Client self.api_client = ApiClient() def get_auth_headers(self, config): - """Constructs a Basic Auth header dictionary from the Configuration. - This dictionary can be used directly with the API client's REST client.""" + """ + Constructs a Basic Auth header dictionary from the Configuration. This + dictionary can be used directly with the API client's REST client. + """ headers = dict() auth = config.auth_settings().get("basic") headers[auth["key"]] = auth["value"] return headers - def _discover_endpoint(self, config): - """Discovers the scheme and version of a potential Cloudara Manager host""" + def discover_endpoint(self, config): + """Discovers the scheme and version of a potential Cloudara Manager host.""" # Get the authentication headers and REST client headers = self.get_auth_headers(config) rest = RESTClientObject() @@ -213,20 +220,17 @@ def call_api(self, path, method, query=None, field="items", body=None): _preload_content=False, ) - if 200 >= results[1] <= 299: - data = json.loads(results[0].data.decode("utf-8")) - if field in data: - data = data[field] - return data if type(data) is list else [data] - else: - self.module.fail_json( - msg="Error interacting with CM resource", status_code=results[1] - ) + data = json.loads(results[0].data.decode("utf-8")) + if field in data: + data = data[field] + return data if type(data) is list else [data] @staticmethod - def ansible_module_discovery(argument_spec={}, required_together=[], **kwargs): - """INTERNAL: Creates the Ansible module argument spec and dependencies for CM API endpoint discovery. - Typically, modules will use the ansible_module method to include direct API endpoint URL support. + def ansible_module_internal(argument_spec={}, required_together=[], **kwargs): + """ + INTERNAL: Creates the Ansible module argument spec and dependencies for + CM API endpoint discovery. Typically, modules will use the + ansible_module method to include direct API endpoint URL support. """ return AnsibleModule( argument_spec=dict( @@ -238,6 +242,7 @@ def ansible_module_discovery(argument_spec={}, required_together=[], **kwargs): verify_tls=dict( required=False, type="bool", default=True, aliases=["tls"] ), + ssl_ca_cert=dict(type="path", aliases=["tls_cert", "ssl_cert"]), username=dict(required=True, type="str"), password=dict(required=True, type="str", no_log=True), debug=dict( @@ -262,8 +267,11 @@ def ansible_module( required_together=[], **kwargs ): - """Creates the base Ansible module argument spec and dependencies, including discovery and direct endpoint URL support.""" - return ClouderaManagerModule.ansible_module_discovery( + """ + Creates the base Ansible module argument spec and dependencies, + including discovery and direct endpoint URL support. + """ + return ClouderaManagerModule.ansible_module_internal( argument_spec=dict( **argument_spec, url=dict(type="str", aliases=["endpoint", "cm_endpoint_url"]), diff --git a/plugins/modules/cm_endpoint_info.py b/plugins/modules/cm_endpoint_info.py index f207dcb8..3acf59b5 100644 --- a/plugins/modules/cm_endpoint_info.py +++ b/plugins/modules/cm_endpoint_info.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # Copyright 2023 Cloudera, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,13 +12,17 @@ # 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 ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: cm_endpoint_info short_description: Discover the Cloudera Manager API endpoint @@ -34,9 +35,9 @@ - cm_client extends_documentation_fragment: - cloudera.cluster.cm_options -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" --- # This will first try 'http://example.cloudera.com:7180' and will # follow any redirects @@ -46,35 +47,34 @@ username: "jane_smith" password: "S&peR4Ec*re" register: cm_endpoint -''' +""" -RETURN = r''' +RETURN = r""" --- endpoint: description: The discovered Cloudera Manager API endpoint type: str returned: always -''' +""" + class ClouderaEndpointInfo(ClouderaManagerModule): def __init__(self, module): super(ClouderaEndpointInfo, self).__init__(module) - + # Initialize the return values self.endpoint = "" - + # Execute the logic self.process() - + @ClouderaManagerModule.handle_process def process(self): self.endpoint = self.api_client.host def main(): - module = ClouderaManagerModule.ansible_module_discovery( - supports_check_mode=True - ) + module = ClouderaManagerModule.ansible_module_internal(supports_check_mode=True) result = ClouderaEndpointInfo(module) @@ -84,13 +84,11 @@ def main(): ) if result.debug: - output.update( - sdk_out=result.log_out, - sdk_out_lines=result.log_lines - ) + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) module.exit_json(**output) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/plugins/modules/cm_resource.py b/plugins/modules/cm_resource.py index 6f172d99..9dca4400 100644 --- a/plugins/modules/cm_resource.py +++ b/plugins/modules/cm_resource.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # Copyright 2023 Cloudera, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,13 +12,17 @@ # 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 ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: cm_resource short_description: Create, update, and delete resources from the Cloudera Manager API endpoint @@ -52,9 +53,9 @@ - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint - cloudera.cluster.cm_resource -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" --- - name: Create a new local Cloudera Manager user cloudera.cluster.cm_resource: @@ -79,15 +80,16 @@ authRoles: - name: "ROLE_LIMITED" -- name: Delete a Cloudera Manager user +- name: Delete a Cloudera Manager user using a custom SSL certificate host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" path: "/user/existing_user" + ssl_ca_cert: "/path/to/ssl_ca.crt" method: "DELETE" -''' +""" -RETURN = r''' +RETURN = r""" --- resources: description: @@ -96,42 +98,48 @@ type: list elements: complex returned: always -''' +""" + class ClouderaResource(ClouderaManagerModule): def __init__(self, module): super(ClouderaResource, self).__init__(module) - + # Set parameters - self.method = self._get_param('method') - self.path = self._get_param('path') - self.query = self._get_param('query', dict()) - self.field = self._get_param('field') - self.body = self._get_param('body') - + self.method = self.get_param("method") + self.path = self.get_param("path") + self.query = self.get_param("query", dict()) + self.field = self.get_param("field") + self.body = self.get_param("body") + # Initialize the return values self.resources = [] - + # Execute the logic self.process() - + @ClouderaManagerModule.handle_process def process(self): if not self.module.check_mode: - self.resources = self.call_api(self.path, self.method, self.query, - self.field, self.body) - - + self.resources = self.call_api( + self.path, self.method, self.query, self.field, self.body + ) + + def main(): module = ClouderaManagerModule.ansible_module( argument_spec=dict( - method=dict(required=True, type='str', choices=['POST', 'PUT', 'DELETE']), - path=dict(required=True, type='str'), - query=dict(required=False, type='dict', aliases=['query_parameters', 'parameters']), - body=dict(required=False, type='dict'), - field=dict(required=False, type='str', default='items', aliases=['return_field']) + method=dict(required=True, type="str", choices=["POST", "PUT", "DELETE"]), + path=dict(required=True, type="str"), + query=dict( + required=False, type="dict", aliases=["query_parameters", "parameters"] + ), + body=dict(required=False, type="dict"), + field=dict( + required=False, type="str", default="items", aliases=["return_field"] + ), ), - supports_check_mode=True + supports_check_mode=True, ) result = ClouderaResource(module) @@ -142,13 +150,11 @@ def main(): ) if result.debug: - output.update( - sdk_out=result.log_out, - sdk_out_lines=result.log_lines - ) + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) module.exit_json(**output) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/plugins/modules/cm_resource_info.py b/plugins/modules/cm_resource_info.py index ec518303..7989cba7 100644 --- a/plugins/modules/cm_resource_info.py +++ b/plugins/modules/cm_resource_info.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # Copyright 2023 Cloudera, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,15 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json - -from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ClouderaManagerModule +from ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: cm_resource_info short_description: Retrieve resources from the Cloudera Manager API endpoint @@ -40,19 +39,28 @@ - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint - cloudera.cluster.cm_resource -''' +""" -EXAMPLES = r''' ---- +EXAMPLES = r""" - name: Gather details about all Cloudera Manager users cloudera.cluster.cm_resource_info: host: example.cloudera.com username: "jane_smith" password: "S&peR4Ec*re" path: "/users" -''' + register: cm_users + +- name: Retrieve details for all running commands on a cluster using a custom SSL certificate + cloudera.cluster.cm_resource_info: + host: example.cloudera.com + username: "jane_smith" + password: "S&peR4Ec*re" + ssl_ca_cert: "/path/to/ssl_ca.crt" + path: "/cluster/example_cluster/commands" + register: running_commands +""" -RETURN = r''' +RETURN = r""" --- resources: description: @@ -61,36 +69,41 @@ type: list elements: complex returned: always -''' +""" + class ClouderaResourceInfo(ClouderaManagerModule): def __init__(self, module): super(ClouderaResourceInfo, self).__init__(module) - + # Set parameters - self.path = self._get_param('path') - self.query = self._get_param('query', dict()) - self.field = self._get_param('field') - + self.path = self.get_param("path") + self.query = self.get_param("query", dict()) + self.field = self.get_param("field") + # Initialize the return values self.resources = [] - + # Execute the logic self.process() - + @ClouderaManagerModule.handle_process def process(self): - self.resources = self.call_api(self.path, 'GET', self.query, self.field) + self.resources = self.call_api(self.path, "GET", self.query, self.field) def main(): module = ClouderaManagerModule.ansible_module( argument_spec=dict( - path=dict(required=True, type='str'), - query=dict(required=False, type='dict', aliases=['query_parameters', 'parameters']), - field=dict(required=False, type='str', default='items', aliases=['return_field']) + path=dict(required=True, type="str"), + query=dict( + required=False, type="dict", aliases=["query_parameters", "parameters"] + ), + field=dict( + required=False, type="str", default="items", aliases=["return_field"] + ), ), - supports_check_mode=True + supports_check_mode=True, ) result = ClouderaResourceInfo(module) @@ -101,13 +114,11 @@ def main(): ) if result.debug: - output.update( - sdk_out=result.log_out, - sdk_out_lines=result.log_lines - ) + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) module.exit_json(**output) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/plugins/modules/cm_version_info.py b/plugins/modules/cm_version_info.py index e50ffc20..f45f679c 100644 --- a/plugins/modules/cm_version_info.py +++ b/plugins/modules/cm_version_info.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # Copyright 2023 Cloudera, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,15 +12,19 @@ # 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 ansible_collections.cloudera.cluster.plugins.module_utils.cm_utils import ( + ClouderaManagerModule, +) from cm_client import ClouderaManagerResourceApi -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: cm_version_info short_description: Gather information about Cloudera Manager @@ -37,9 +38,9 @@ extends_documentation_fragment: - cloudera.cluster.cm_options - cloudera.cluster.cm_endpoint -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" --- - name: Gather details using an endpoint URL cloudera.cluster.cm_version: @@ -56,16 +57,16 @@ username: "jane_smith" password: "S&peR4Ec*re" register: cm_discovery -''' +""" -RETURN = r''' +RETURN = r""" --- -version: +cloudera_manager: description: Details for the Cloudera Manager instance type: dict contains: version: - description: The CM version. + description: The Cloudera Manager version. type: str returned: optional snapshot: @@ -84,44 +85,41 @@ description: Source control management hash. type: str returned: optional -''' +""" + class ClouderaManagerVersionInfo(ClouderaManagerModule): def __init__(self, module): super(ClouderaManagerVersionInfo, self).__init__(module) - + # Initialize the return values - self.cm = dict() - + self.version = dict() + # Execute the logic self.process() - + @ClouderaManagerModule.handle_process def process(self): api_instance = ClouderaManagerResourceApi(self.api_client) - self.cm=api_instance.get_version().to_dict() + self.version = api_instance.get_version().to_dict() def main(): - module = ClouderaManagerModule.ansible_module( - supports_check_mode=True - ) + module = ClouderaManagerModule.ansible_module(supports_check_mode=True) result = ClouderaManagerVersionInfo(module) output = dict( changed=False, - cm=result.cm, + cloudera_manager=result.version, ) if result.debug: - output.update( - sdk_out=result.log_out, - sdk_out_lines=result.log_lines - ) + log = result.log_capture.getvalue() + output.update(debug=log, debug_lines=log.split("\n")) module.exit_json(**output) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() From 23c1270d08c8338c2f0ce117afe3d90806622caf Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 30 Nov 2023 10:51:54 -0500 Subject: [PATCH 2/3] Remove unused import Signed-off-by: Webster Mudge --- plugins/modules/assemble_cluster_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/assemble_cluster_template.py b/plugins/modules/assemble_cluster_template.py index 39b64c8b..7710c6d5 100644 --- a/plugins/modules/assemble_cluster_template.py +++ b/plugins/modules/assemble_cluster_template.py @@ -138,7 +138,7 @@ import tempfile from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.common.text.converters import to_native, to_text +from ansible.module_utils.common.text.converters import to_native class AssembleClusterTemplate(object): From fee8ed2fd7601f4d072f411566fa195c3bfc9fe3 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 30 Nov 2023 13:23:58 -0500 Subject: [PATCH 3/3] Update to normalize redirects during endpoint discovery Update logging to use the root logger and add a specific logger for cloudera.cluster Signed-off-by: Webster Mudge --- plugins/module_utils/cm_utils.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/cm_utils.py b/plugins/module_utils/cm_utils.py index 3818c0aa..c0d8af01 100644 --- a/plugins/module_utils/cm_utils.py +++ b/plugins/module_utils/cm_utils.py @@ -115,19 +115,20 @@ def __init__(self, module): ) # Configure the urllib3 logger - self.logger = logging.getLogger("urllib3") + self.logger = logging.getLogger("cloudera.cluster") if self.debug: - self.logger.setLevel(logging.DEBUG) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + root_logger.propagate = True self.log_capture = io.StringIO() handler = logging.StreamHandler(self.log_capture) - handler.setLevel(logging.DEBUG) formatter = logging.Formatter(log_format) handler.setFormatter(formatter) - self.logger.addHandler(handler) + root_logger.addHandler(handler) self.logger.debug("CM API agent: %s", self.agent_header) @@ -180,7 +181,15 @@ def discover_endpoint(self, config): rendered = rest.pool_manager.request( "GET", pre_rendered.url, headers=headers.copy() ) - rendered_url = rendered.geturl() + + # Normalize to handle redirects + try: + rendered_url = rendered.url + except Exception: + rendered_url = rendered.geturl() + + if rendered_url == "/": + rendered_url = pre_rendered.url # Discover API version if not set if not self.version: