|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +# Copyright 2023 Cloudera, Inc. |
| 5 | +# |
| 6 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +# you may not use this file except in compliance with the License. |
| 8 | +# You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, software |
| 13 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +# See the License for the specific language governing permissions and |
| 16 | +# limitations under the License. |
| 17 | + |
| 18 | +""" |
| 19 | +A common Ansible plugin functions for Cloudera Manager |
| 20 | +""" |
| 21 | + |
| 22 | +import json |
| 23 | +import logging |
| 24 | + |
| 25 | +from urllib3 import disable_warnings |
| 26 | +from urllib3.exceptions import InsecureRequestWarning, MaxRetryError, HTTPError |
| 27 | +from urllib3.util import Url |
| 28 | +from urllib.parse import urljoin |
| 29 | + |
| 30 | +from ansible.errors import AnsibleError |
| 31 | +from ansible.module_utils.common.text.converters import to_text |
| 32 | +from ansible.plugins.lookup import LookupBase |
| 33 | + |
| 34 | +from cm_client import ApiClient, Configuration |
| 35 | +from cm_client.rest import ApiException, RESTClientObject |
| 36 | + |
| 37 | + |
| 38 | +__maintainer__ = [ "[email protected]"] |
| 39 | + |
| 40 | + |
| 41 | +""" |
| 42 | +A common Ansible Lookup plugin for API access to Cloudera Manager. |
| 43 | +""" |
| 44 | + |
| 45 | +class ClouderaManagerLookupBase(LookupBase): |
| 46 | + def initialize_client(self): |
| 47 | + # Set up core CM API client parameters |
| 48 | + config = Configuration() |
| 49 | + config.username = self.get_option("username") |
| 50 | + config.password = self.get_option("password") |
| 51 | + config.verify_ssl = self.get_option("verify_tls") |
| 52 | + config.debug = self.get_option("debug") |
| 53 | + |
| 54 | + # Configure logging |
| 55 | + _log_format = ( |
| 56 | + "%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s" |
| 57 | + ) |
| 58 | + if self.get_option("debug"): |
| 59 | + self._setup_logger(logging.DEBUG, _log_format) |
| 60 | + self.logger.debug("CM API agent: %s", self.get_option("agent_header")) |
| 61 | + else: |
| 62 | + self._setup_logger(logging.ERROR, _log_format) |
| 63 | + |
| 64 | + if self.get_option("verify_tls") is False: |
| 65 | + disable_warnings(InsecureRequestWarning) |
| 66 | + |
| 67 | + # If provided a CM API endpoint URL, use it directly |
| 68 | + if self.get_option("endpoint"): |
| 69 | + config.host = self.get_option("endpoint") |
| 70 | + # Otherwise, run discovery on missing parts |
| 71 | + else: |
| 72 | + config.host = self._discover_endpoint(config) |
| 73 | + |
| 74 | + self.api_client = ApiClient() |
| 75 | + |
| 76 | + def _setup_logger(self, log_level, log_format): |
| 77 | + """Configures the logging of the HTTP activity""" |
| 78 | + self.logger = logging.getLogger("urllib3") |
| 79 | + self.logger.setLevel(log_level) |
| 80 | + |
| 81 | + def _get_auth_headers(self, config): |
| 82 | + """Constructs a Basic Auth header dictionary from the Configuration. |
| 83 | + This dictionary can be used directly with the API client's REST client.""" |
| 84 | + headers = dict() |
| 85 | + auth = config.auth_settings().get("basic") |
| 86 | + headers[auth["key"]] = auth["value"] |
| 87 | + return headers |
| 88 | + |
| 89 | + def _discover_endpoint(self, config): |
| 90 | + """Discovers the scheme and version of a potential Cloudara Manager host""" |
| 91 | + # Get the authentication headers and REST client |
| 92 | + headers = self._get_auth_headers(config) |
| 93 | + rest = RESTClientObject() |
| 94 | + |
| 95 | + # Resolve redirects to establish HTTP scheme and port |
| 96 | + pre_rendered = Url( |
| 97 | + scheme="https" if self.get_option("force_tls") else "http", |
| 98 | + host=self.get_option("host"), |
| 99 | + port=self.get_option("port"), |
| 100 | + ) |
| 101 | + rendered = rest.pool_manager.request( |
| 102 | + "GET", pre_rendered.url, headers=headers.copy() |
| 103 | + ) |
| 104 | + rendered_url = rendered.geturl() |
| 105 | + |
| 106 | + # Discover API version if not set |
| 107 | + if not self.version: |
| 108 | + pre_versioned = urljoin(rendered_url, "/api/version") |
| 109 | + versioned = rest.pool_manager.request("GET", pre_versioned, headers=headers) |
| 110 | + self.version = versioned.data.decode("utf-8") |
| 111 | + |
| 112 | + # Construct the discovered API endpoint |
| 113 | + return urljoin(rendered_url, "/api/" + self.version) |
| 114 | + |
| 115 | + def get(self, path, query=None, field="items", body=None): |
| 116 | + """Wrapper to GET a CM API endpoint path directly.""" |
| 117 | + path_params = [] |
| 118 | + header_params = {} |
| 119 | + header_params["Accept"] = self.api_client.select_header_accept( |
| 120 | + ["application/json"] |
| 121 | + ) |
| 122 | + header_params["Content-Type"] = self.api_client.select_header_content_type( |
| 123 | + ["application/json"] |
| 124 | + ) |
| 125 | + |
| 126 | + try: |
| 127 | + results = self.api_client.call_api( |
| 128 | + path, |
| 129 | + "GET", |
| 130 | + path_params, |
| 131 | + query, |
| 132 | + header_params, |
| 133 | + auth_settings=["basic"], |
| 134 | + _preload_content=False, |
| 135 | + ) |
| 136 | + |
| 137 | + if 200 >= results[1] <= 299: |
| 138 | + data = json.loads(results[0].data.decode("utf-8")) |
| 139 | + if field in data: |
| 140 | + data = data[field] |
| 141 | + return data if type(data) is list else [data] |
| 142 | + else: |
| 143 | + raise AnsibleError( |
| 144 | + "Error interacting with CM resource. Status code: %s" |
| 145 | + % to_text(results[1]) |
| 146 | + ) |
| 147 | + except ApiException as ae: |
| 148 | + body = ae.body.decode("utf-8") |
| 149 | + if body != "": |
| 150 | + body = json.loads(body) |
| 151 | + raise AnsibleError( |
| 152 | + "API error: %s; Status code: %s" % (ae.reason, ae.status), |
| 153 | + obj=body, |
| 154 | + orig_exc=ae, |
| 155 | + ) |
| 156 | + except MaxRetryError as maxe: |
| 157 | + raise AnsibleError("Request error: %s" % to_text(maxe.reason)) |
| 158 | + except HTTPError as he: |
| 159 | + raise AnsibleError("HTTP request error", orig_exc=he) |
0 commit comments