Source code for solidfire.common

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright © 2014-2016 NetApp, Inc. All Rights Reserved.
#
# CONFIDENTIALITY NOTICE: THIS SOFTWARE CONTAINS CONFIDENTIAL INFORMATION OF
# NETAPP, INC. USE, DISCLOSURE OR REPRODUCTION IS PROHIBITED WITHOUT THE PRIOR
# EXPRESS WRITTEN PERMISSION OF NETAPP, INC.
"""API Common Library"""

import itertools
import json
import logging

import requests
from requests.auth import HTTPBasicAuth
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

from solidfire.common import model

LOG = logging.getLogger('solidfire.Element')
LOG.setLevel(logging.INFO)
CH = logging.StreamHandler()
CH.setLevel(logging.INFO)
FORMATTER = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
CH.setFormatter(FORMATTER)
LOG.addHandler(CH)

ATOMIC_COUNTER = itertools.count()


[docs]def setLogLevel(level): """ Set the logging level of Element logger and all handlers. >>> from logging >>> from solidfire import common >>> common.setLogLevel(logging.DEBUG) :param level: level must be an int or a str. """ LOG.setLevel(level) for handler in LOG.handlers: handler.setLevel(level)
[docs]class SdkOperationError(Exception): def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs)
[docs]class ApiServerError(Exception): """ ApiServerError is an exception that occurs on the server and is passes as a response back to the sdk. """ def __init__(self, method_name, err_json): """ ApiServerError constructor. :param method_name: name of the service method where the error occurred. :type method_name: str :param err_json: the json formatted error received from the service. :type err_json: str """ try: json.loads(err_json) except: err_json = '{}' self._method_name = method_name self._err_json = err_json Exception.__init__(self) def __repr__(self): return '%s(method_name="%s", err_json=%s)' % ( self.__class__.__name__, self._method_name, self._err_json ) def __str__(self): return repr(self) @property def method_name(self): """The name of the service method causing the error.""" return self._method_name @property def error_name(self): """The name of the error.""" maybeDict = json.loads(self._err_json) if isinstance(maybeDict, str): maybeDict = json.loads(maybeDict) return maybeDict.get('error', {}).get('name', 'Unknown') @property def error_code(self): """The numeric code for this error.""" maybeDict = json.loads(self._err_json) if isinstance(maybeDict, str): maybeDict = json.loads(maybeDict) return int(maybeDict.get('error', {}).get('code', 500)) @property def message(self): """A user-friendly message returned from the server.""" try: json_err = json.loads(self._err_json) return json_err.get('error', {}).get('message', None) except: return self._err_json
[docs]class ApiMethodVersionError(Exception): """ An ApiMethodVersionError occurs when a service method is not compatible with the version of the connected server. """ def __init__(self, method_name, api_version, since, deprecated=None): """ ApiMethodVersionError constructor. :param method_name: name of the service method where the error occurred. :type method_name: str :param api_version: the version of API used to instantiate the connection to the server. :type api_version: str or float :param since: the earliest version of the API a service method is compatible. :type since: str or float :param deprecated: the latest version of the API that a method is compatible. :type deprecated: str or float """ self._method_name = method_name self._api_version = float(api_version) self._since = float(since) if since is not None else since self._deprecated = float(deprecated) \ if deprecated is not None else deprecated Exception.__init__(self) def __repr__(self): return '%s(method_name="%s", ' \ 'api_version=%s, ' \ 'since=%s, ' \ 'deprecated=%s) ' % ( self.__class__.__name__, self._method_name, self._api_version, self._since, self._deprecated ) def __str__(self): return str.format( '\n Invalid Method:\n' ' Method Name: {_method_name}\n' ' Service Api Version: {_api_version}\n' ' Method Exists Since: {_since}\n' ' Method Deprecated: {_deprecated}\n', **self.__dict__ ) @property def method_name(self): """The name of the service method causing the error.""" return self._method_name @property def api_version(self): """The version of the Element API Service""" return self._api_version @property def since(self): """The version a service was introduced""" return self._since @property def deprecated(self): """The version a service was deprecated""" return self._deprecated
[docs]class ApiParameterVersionError(Exception): """ An ApiParameterVersionError occurs when a parameter supplied to a service method is not compatible with the version of the connected server. """ def __init__(self, method_name, api_version, params): """ ApiParameterVersionError constructor. :param method_name: name of the service method where the error occurred. :type method_name: str :param api_version: the version of API used to instantiate the connection to the server. :type api_version: str or float :param params: the list of incompatible parameters provided to a service method call. This tuple should include name, value, since, and deprecated values for each offending parameter. :type params: list of tuple """ self._method_name = method_name self._api_version = float(api_version) self._params = params self._violations = [] if params is not None: for (name, value, since, deprecated) in params: self._violations.append( name + ' (version: ' + str(since) + ')' ) Exception.__init__(self) def __repr__(self): return '%s(method_name="%s", ' \ 'api_version=%s, ' \ 'params=%s) ' % ( self.__class__.__name__, self._method_name, self._api_version, self._params, ) def __str__(self): return str.format( '\n Invalid Parameter:\n' ' Method: {_method_name}\n' ' Api Version: {_api_version}\n' ' Invalid Parameters: {_violations}\n', **self.__dict__ ) @property def method_name(self): """The name of the service method causing the error.""" return self._method_name @property def api_version(self): """The version of the Element API Service""" return self._api_version @property def params(self): """The parameters checked with a service call""" return self._params @property def violations(self): """The parameters violated with the service call""" return self._violations
[docs]class ApiVersionExceededError(Exception): """ An ApiVersionExceededError occurs when connecting to a server with a version lower then the provided api_version. """ def __init__(self, api_version, current_version): """ ApiVersionExceededError constructor. :param api_version: the version of API used to instantiate the connection to the server. :type api_version: str or float :param current_version: the current version of the server. :type current_version: float """ self._api_version = float(api_version) self._current_version = float(current_version) Exception.__init__(self) def __repr__(self): return '%s(api_version=%s, ' \ 'current_version=%s) ' % ( self.__class__.__name__, self._api_version, self._current_version ) def __str__(self): return str.format( '\n Version Exceeded:\n' ' Provided Api Version: {_api_version}\n' ' Max Version: {current_version}\n', current_version=self._current_version, **self.__dict__ ) @property def api_version(self): """The version of the Element API Service""" return self._api_version @property def current_version(self): """The current version of the connected Element OS""" return self._current_version
[docs]class ApiVersionUnsupportedError(Exception): """ An ApiVersionUnsupportedError occurs when connecting to a server unable to support the provided api_version. """ def __init__(self, api_version, supported_versions): """ ApiVersionUnsupportedError constructor. :param api_version: the version of API used to instantiate the connection to the server. :type api_version: str or float :param supported_versions: the list of supported versions provided by a server. :type supported_versions: float[] """ self._api_version = float(api_version) self._supported_versions = [float(i) for i in supported_versions] Exception.__init__(self) def __repr__(self): return '%s(api_version=%s, ' \ 'supported_versions=%s)' % ( self.__class__.__name__, self._api_version, self._supported_versions ) def __str__(self): return str.format( '\n Version Unsupported:\n' ' Provided Api Version: {_api_version}\n' ' Supported Version: {_supported_versions}\n', **self.__dict__ ) @property def api_version(self): """The version of the Element API Service""" return self._api_version @property def supported_versions(self): """The versions supported by the connected Element OS""" return self._supported_versions
[docs]class ApiConnectionError(Exception): def __init__(self, message): super(ApiConnectionError, self).__init__(message)
[docs]class CurlDispatcher(object): """ The CurlDispatcher is responsible for connecting, sending, and receiving data to a server. """ def __init__(self, endpoint, username, password, verify_ssl): """ The CurlDispatcher constructor. :param endpoint: the server URL :type endpoint: str :param username: the username for authentication :type username: str :param password: the password for authentication :type password: str :param verify_ssl: If True, ssl errors will cause an exception to be raised, otherwise, if False, they are ignored. :type verify_ssl: bool """ self._endpoint = endpoint self._username = username self._password = password self._verify_ssl = verify_ssl self._timeout = 300 self._connect_timeout = 30
[docs] def timeout(self, timeout_in_sec): """ Set the time to wait for a response before timeout. :param timeout_in_sec: the read timeout in seconds. :type timeout_in_sec: int :raise ValueError: if timeout_in_sec is less than 0 """ temp_timeout = int(timeout_in_sec) if temp_timeout < 0: raise ValueError("Read Timeout less than 0") self._timeout = temp_timeout
[docs] def connect_timeout(self, timeout_in_sec): """ Set the time to wait for a connection to be established before timeout. :param timeout_in_sec: the connection timeout in seconds. :type timeout_in_sec: int :raise ValueError: if timeout_in_sec is less than 0 """ temp_timeout = int(timeout_in_sec) if temp_timeout < 0: raise ValueError("Connection Timeout less than 0") self._connect_timeout = temp_timeout
[docs] def restore_timeout_defaults(self): """ Restores the Connection and Read Timeout to their original durations of 30 seconds for connection timeout and 300 seconds (5 minutes) for read timeout. """ self._timeout = 300 self._connect_timeout = 30
[docs] def post(self, data): """ Post data to the associated endpoint and await the server's response. :param data: the data to be posted. :type data: str or json """ auth = None if self._username is None or self._password is None: raise ValueError("Username or Password is not set") else: auth = HTTPBasicAuth(self._username, self._password) resp = requests.post(self._endpoint, data=data, json=None, verify=self._verify_ssl, timeout=self._timeout, auth=auth) if resp.text == '': return {"code": resp.status_code, "name": resp.reason, "message": ""} return resp.text
[docs]class ServiceBase(object): """ The base type for API services. This performs the sending, encoding and decoding of requests. """ def __init__(self, mvip=None, username=None, password=None, api_version=8.0, verify_ssl=True, dispatcher=None): """ Constructor for initializing a connection to an instance of Element OS :param mvip: the management IP (IP or hostname) :type mvip: str :param username: username use to connect to the Element OS instance. :type username: str :param password: authentication for username :type password: str :param api_version: specific version of Element OS to connect. :type api_version: float :param verify_ssl: disable to avoid ssl connection errors especially when using an IP instead of a hostname :type verify_ssl: bool :param dispatcher: a prebuilt or custom http dispatcher :return: a configured connection to an Element OS instance """ self._api_version = float(api_version) self._private_keys = ["clusterPairingKey", "volumePairingKey", "password", "initiatorSecret", "scriptParameters", "targetSecret", "searchBindPassword"] endpoint = str.format('https://{mvip}/json-rpc/{api_version}', mvip=mvip, api_version=self._api_version) if 'https' in endpoint: self._port = 443 else: self._port = '' if not dispatcher: dispatcher = CurlDispatcher(endpoint, username, password, verify_ssl) self._dispatcher = dispatcher if mvip is not None: mvipArr = mvip.split(':') if len(mvipArr) == 2: self._port = mvipArr[1]
[docs] def timeout(self, timeout_in_sec): """ Set the time to wait for a response before timeout. :param timeout_in_sec: the read timeout in seconds. :type timeout_in_sec: int :raise ValueError: if timeout_in_sec is less than 0 """ self._dispatcher.timeout(timeout_in_sec)
[docs] def connect_timeout(self, timeout_in_sec): """ Set the time to wait for a connection to be established before timeout. :param timeout_in_sec: the connection timeout in seconds. :type timeout_in_sec: int :raise ValueError: if timeout_in_sec is less than 0 """ self._dispatcher.connect_timeout(timeout_in_sec)
[docs] def restore_timeout_defaults(self): """ Restores the Connection and Read Timeout to their original durations of 300 seconds (5 minutes) each. """ self._dispatcher.restore_timeout_defaults()
@property def api_version(self): """ Returns the version of the Element API :return: the version of the Element API :rtype: float """ return self._api_version
[docs] def send_request(self, method_name, result_type, params=None, since=None, deprecated=None, return_response_raw=False): """ :param method_name: the name of the API method to call :type method_name: str :param result_type: the type of the result object returned from the API method called. :type result_type: DataObject :param params: the parameters supplied to the API call. :type params: dict :param since: the first version this service was available :type since: str or float :param deprecated: the final version this service was available :type deprecated: str or float :return: the result of the API service call :rtype: DataObject """ self._check_method_version(method_name, since, deprecated) if params is None: params = {} global ATOMIC_COUNTER if hasattr(ATOMIC_COUNTER, 'next'): atomic_id = ATOMIC_COUNTER.next() else: atomic_id = ATOMIC_COUNTER.__next__() request_dict = { 'method': method_name, 'id': atomic_id if atomic_id > 0 else 0, 'params': dict( (name, model.serialize(val)) for name, val in params.items() ), } obfuscated_request_raw = json.dumps(self._obfuscate_keys(request_dict)) encoded = json.dumps(request_dict) try: LOG.info(msg=obfuscated_request_raw) response_raw = self._dispatcher.post(encoded) except requests.ConnectionError as e: if ("Errno 8" in str(e)): raise ApiConnectionError("Unknown host based on target.") elif ("Errno 60" in str(e)): raise ApiConnectionError("Connection timed out.") elif ("Errno 61" in str(e)): raise ApiConnectionError("Connection Refused. Confirm your target is a SolidFire cluster or node.") elif ("Errno 51" in str(e)): raise ApiConnectionError("Network is unreachable") raise ApiConnectionError(e) except requests.exceptions.ChunkedEncodingError as error: raise ApiConnectionError(error.args) except Exception as error: raise ApiServerError(method_name, error) # noinspection PyBroadException LOG.debug(msg=response_raw) if isinstance(response_raw, dict): #if isinstance(response_raw, dict) and "name" in response_raw and "code" in response_raw: response = { 'error': response_raw } else: try: response = json.loads(response_raw) LOG.debug(msg=response_raw) except Exception as error: LOG.error(msg=response_raw) if "401 Unauthorized." in response_raw: raise ApiConnectionError("Bad Credentials") if "404 Not Found" in response_raw: raise ApiConnectionError("404 Not Found") response = { 'error': { 'name': 'JSONDecodeError', 'code': 400, 'message': str(error) } } if return_response_raw: return response_raw if 'error' in response: if response["error"]["code"] == 400: raise requests.HTTPError(str(response["error"]["code"]) + " " + response["error"]["name"] + " " + response["error"]["message"]) else: raise ApiServerError(method_name, json.dumps(response)) else: return model.extract(result_type, response['result'])
# For logging purposes, there are a set of keys we don't want to be in plain text. # This goes through the response and obfuscates the secret keys. def _obfuscate_keys(self, response, obfuscate = False): if type(response) == dict: private_dict = dict() for key in response: if key in self._private_keys: private_dict[key] = self._obfuscate_keys(response[key], True) else: private_dict[key] = self._obfuscate_keys(response[key]) return private_dict if type(response) == list: return [self._obfuscate_keys(item) for item in response] if obfuscate: return "*****" else: return response def _check_connection_type(self, method_name, connection_type): """ Check the connection type to verify that it is right. :param connection_type: connection type the method expects. :type connection_type: str """ if(connection_type == "Cluster" and int(self._port) == 442): error = method_name+" cannot be called on a node connection. It is a cluster-only method." raise ApiConnectionError(error) elif(connection_type == "Node" and int(self._port) == 443): error = method_name+" cannot be called on a cluster connection. It is a node-only method." raise ApiConnectionError(error) def _check_method_version(self, method_name, since, deprecated=None): """ Check method version against the initialized api_version of the service. :param method_name: service method name performing the check. :type method_name: str :param since: service method inception version :type since: float or str :param deprecated: service method deprecation version :type deprecated: float or str :raise ApiMethodVersionError: if the configured version of the ServiceBase is less then the inception version. Deprecation is not currently checked. """ if since is not None and float(since) > self._api_version: raise ApiMethodVersionError(method_name, self._api_version, since=float(since), deprecated=deprecated) def _check_param_versions(self, method_name, params): """ Checks parameters against the initialized api_version of the service. :param method_name: service method name performing the check. :type method_name: str :param params: the list of versioned parameters, their value, inception version, and optionally, their deprecation version as a tuple :type params: list of tuple :raise ApiParameterVersionError: if the configured version of the ServiceBase is less then the inception version of the parameter. Deprecation is not currently checked. """ invalid = [] if params is None: params = [] for (name, value, since, deprecated) in params: if value is not None and float(since) > self._api_version: invalid.append((name, value, float(since), deprecated)) if len(invalid) > 0: raise ApiParameterVersionError(method_name, self._api_version, invalid)