Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions plugins/lookup/cm_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright 2023 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.

import io
import json
import logging

from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning, MaxRetryError, HTTPError
from urllib3.util import Url

from ansible.errors import AnsibleError
from ansible.module_utils.common.text.converters import to_text, to_native
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display

from cm_client import ApiClient, Configuration
from cm_client.rest import ApiException, RESTClientObject

display = Display()

"""
A common Ansible Lookup plugin for API access to Cloudera Manager.
"""
class ClouderaManagerLookupBase(LookupBase):
def initialize_client(self):
# Set up core CM API client parameters
config = Configuration()
config.username = self.get_option('username')
config.password = self.get_option('password')
config.verify_ssl = self.get_option('verify_tls')
config.debug = self.get_option('debug')

# Configure logging
_log_format = '%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s'
if self.get_option('debug'):
self._setup_logger(logging.DEBUG, _log_format)
self.logger.debug("CM API agent: %s", self.get_option('agent_header'))
else:
self._setup_logger(logging.ERROR, _log_format)

if self.get_option('verify_tls') is False:
disable_warnings(InsecureRequestWarning)

# If provided a CM API endpoint URL, use it directly
if self.get_option('endpoint'):
config.host = self.get_option('endpoint')
# Otherwise, run discovery on missing parts
else:
config.host = self._discover_endpoint(config)

self.api_client = ApiClient()

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)

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."""
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"""
# Get the authentication headers and REST client
headers = self._get_auth_headers(config)
rest = RESTClientObject()

# Resolve redirects to establish HTTP scheme and port
pre_rendered = Url(scheme="https" if self.get_option('force_tls') else "http", host=self.get_option('host'), port=self.get_option('port'))
rendered = rest.pool_manager.request('GET', pre_rendered.url, headers=headers.copy())
rendered_url = rendered.geturl()

# Discover API version if not set
if not self.version:
pre_versioned = urljoin(rendered_url, "/api/version")
versioned = rest.pool_manager.request('GET', pre_versioned, headers=headers)
self.version = versioned.data.decode('utf-8')

# Construct the discovered API endpoint
return urljoin(rendered_url, "/api/" + self.version)

def get(self, path, query=None, field='items', body=None):
"""Wrapper to GET a CM API endpoint path directly."""
path_params = []
header_params = {}
header_params['Accept'] = self.api_client.select_header_accept(['application/json'])
header_params['Content-Type'] = self.api_client.select_header_content_type(['application/json'])

try:
results =self.api_client.call_api(
path, "GET", path_params, query,
header_params, auth_settings=['basic'],
_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:
raise AnsibleError("Error interacting with CM resource. Status code: %s" % to_text(results[1]))
except ApiException as ae:
body = ae.body.decode('utf-8')
if body != "":
body = json.loads(body)
raise AnsibleError("API error: %s; Status code: %s" % (ae.reason, ae.status), obj=body, orig_exc=ae)
except MaxRetryError as maxe:
raise AnsibleError("Request error: %s" % to_text(maxe.reason))
except HTTPError as he:
raise AnsibleError("HTTP request error", orig_exc=he)
120 changes: 120 additions & 0 deletions plugins/lookup/cm_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright 2023 Cloudera, Inc.
#
# 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

DOCUMENTATION = '''
lookup: cm_service
author: Webster Mudge (@wmudge) <[email protected]>
short_description: Get the details for a service on a CDP Datahub cluster
description:
- Allows you to retrieve the name or full details for a given service on a CDP Datahub cluster.
- If no service name is found on the specified cluster, the lookup returns the value of I(default).
- Otherwise, the lookup entry will be an empty list.
- If the cluster is not found or is ambigious, the lookup will return an error.
- If the Cloudera Manager endpoint is not found or is not available, the lookup will return an error.
options:
_terms:
description:
- A C(service) or list of services to lookup within the CDP Datahub cluster.
required: True
sample:
- KUDU
cluster:
description: Name of the Datahub cluster to query.
type: string
required: True
detailed:
description: Whether to return the full details of the service or just the name.
type: boolean
default: False
username:
description: Username for accessing the Cloudera Manager API.
type: string
required: True
password:
description: Password for accessing the Cloudera Manager API.
type: string
required: True
no_log: True
endpoint:
description: API endpoint of Cloudera Manager.
type: string
required: False
force_tls:
description:
- Whether to force the HTTPS scheme when discovering the Cloudera Manager API endpoint.
- Ignored if C(endpoint) is defined.
type: boolean
default: True
host:
description:
- Hostname when discovering the Cloudera Manager API endpoint.
- Ignored if C(endpoint) is defined.
type: string
port:
description:
- Port when discovering the Cloudera Manager API endpoint.
- Ignored if C(endpoint) is defined.
type: integer
default: 7183
verify_tls:
description: Whether to verify the TLS credentials of the Cloudera Manager API endpoint.
type: boolean
default: True
debug:
description: Whether to log the I(urllib) connection details.
type: boolean
default: False
default:
description: Value to return if no service is found on the cluster.
type: any
version:
description: Version number of the Cloudera Manager API.
type: string
default: v40
agent_header:
description: Header string to identify the connection.
type: string
default: cm_service
notes:
- Requires C(cm_client).
'''

from ansible_collections.cloudera.cluster.plugins.lookup.cm_api import ClouderaManagerLookupBase

from ansible.utils.display import Display

display = Display()

class LookupModule(ClouderaManagerLookupBase):
def run(self, terms, variables=None, **kwargs):
self.set_options(var_options=variables, direct=kwargs)

self.initialize_client()
all_services = {service['type']:service for service in self.get("%s/clusters/%s/services" % (self.get_option('version'), self.get_option('cluster')))}

results = []
for term in LookupModule._flatten(terms):
if term in all_services:
results.append(all_services[term] if self.get_option('detailed') else all_services[term]['name'])
else:
if self.get_option('default') is not None:
results.append(self.get_option('default'))
elif self.get_option('detailed'):
results.append({})
else:
results.append("")
return results