diff --git a/watcherclient/client.py b/watcherclient/client.py index d044a06..6627ff4 100644 --- a/watcherclient/client.py +++ b/watcherclient/client.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # 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 @@ -12,112 +10,176 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient.v2_0 import client as ksclient -import oslo_i18n +from keystoneauth1 import loading as kaloading + +from oslo_utils import importutils from watcherclient._i18n import _ -from watcherclient.common import utils -from watcherclient import exceptions as exc - -oslo_i18n.install('watcherclient') +from watcherclient.common import api_versioning +from watcherclient import exceptions -def _get_ksclient(**kwargs): - """Get an endpoint and auth token from Keystone. - - :param kwargs: keyword args containing credentials: - * username: name of user - * password: user's password - * auth_url: endpoint to authenticate against - * insecure: allow insecure SSL (no cert verification) - * tenant_{name|id}: name or ID of tenant - """ - return ksclient.Client(username=kwargs.get('username'), - password=kwargs.get('password'), - tenant_id=kwargs.get('tenant_id'), - tenant_name=kwargs.get('tenant_name'), - auth_url=kwargs.get('auth_url'), - insecure=kwargs.get('insecure')) - - -def _get_endpoint(client, **kwargs): - """Get an endpoint using the provided keystone client.""" - attr = None - filter_value = None - if kwargs.get('region_name'): - attr = 'region' - filter_value = kwargs.get('region_name') - return client.service_catalog.url_for( - service_type=kwargs.get('service_type') or 'infra-optim', - attr=attr, - filter_value=filter_value, - endpoint_type=kwargs.get('endpoint_type') or 'publicURL') - - -def get_client(api_version, **kwargs): - """Get an authenticated client, based on the credentials in args. +def get_client(api_version, os_auth_token=None, watcher_url=None, + os_username=None, os_password=None, os_auth_url=None, + os_project_id=None, os_project_name=None, os_tenant_id=None, + os_tenant_name=None, os_region_name=None, + os_user_domain_id=None, os_user_domain_name=None, + os_project_domain_id=None, os_project_domain_name=None, + os_service_type=None, os_endpoint_type=None, + insecure=None, timeout=None, os_cacert=None, ca_file=None, + os_cert=None, cert_file=None, os_key=None, key_file=None, + os_watcher_api_version=None, max_retries=None, + retry_interval=None, session=None, os_endpoint_override=None, + **ignored_kwargs): + """Get an authenticated client, based on the credentials. :param api_version: the API version to use. Valid value: '1'. - :param kwargs: keyword args containing credentials, either: - * os_auth_token: pre-existing token to re-use - * watcher_url: watcher API endpoint - or: - * os_username: name of user - * os_password: user's password - * os_auth_url: endpoint to authenticate against - * insecure: allow insecure SSL (no cert verification) - * os_tenant_{name|id}: name or ID of tenant + :param os_auth_token: pre-existing token to re-use + :param watcher_url: watcher API endpoint + :param os_username: name of a user + :param os_password: user's password + :param os_auth_url: endpoint to authenticate against + :param os_project_id: ID of a project + :param os_project_name: name of a project + :param os_tenant_id: ID of a tenant (deprecated in favour of + os_project_id) + :param os_tenant_name: name of a tenant (deprecated in favour of + os_project_name) + :param os_region_name: name of a keystone region + :param os_user_domain_id: ID of a domain the user belongs to + :param os_user_domain_name: name of a domain the user belongs to + :param os_project_domain_id: ID of a domain the project belongs to + :param os_project_domain_name: name of a domain the project belongs to + :param os_service_type: the type of service to lookup the endpoint for + :param os_endpoint_type: the type (exposure) of the endpoint + :param insecure: allow insecure SSL (no cert verification) + :param timeout: allows customization of the timeout for client HTTP + requests + :param os_cacert: path to cacert file + :param ca_file: path to cacert file, deprecated in favour of os_cacert + :param os_cert: path to cert file + :param cert_file: path to cert file, deprecated in favour of os_cert + :param os_key: path to key file + :param key_file: path to key file, deprecated in favour of os_key + :param os_watcher_api_version: watcher API version to use + :param max_retries: Maximum number of retries in case of conflict error + :param retry_interval: Amount of time (in seconds) between retries in case + of conflict error + :param session: Keystone session to use + :param os_endpoint_override: watcher API endpoint + :param ignored_kwargs: all the other params that are passed. Left for + backwards compatibility. They are ignored. """ - - if kwargs.get('os_auth_token') and kwargs.get('watcher_url'): - token = kwargs.get('os_auth_token') - endpoint = kwargs.get('watcher_url') - auth_ref = None - elif (kwargs.get('os_username') and - kwargs.get('os_password') and - kwargs.get('os_auth_url') and - (kwargs.get('os_tenant_id') or kwargs.get('os_tenant_name'))): - - ks_kwargs = { - 'username': kwargs.get('os_username'), - 'password': kwargs.get('os_password'), - 'tenant_id': kwargs.get('os_tenant_id'), - 'tenant_name': kwargs.get('os_tenant_name'), - 'auth_url': kwargs.get('os_auth_url'), - 'service_type': kwargs.get('os_service_type'), - 'endpoint_type': kwargs.get('os_endpoint_type'), - 'insecure': kwargs.get('insecure'), - } - _ksclient = _get_ksclient(**ks_kwargs) - token = (kwargs.get('os_auth_token') - if kwargs.get('os_auth_token') - else _ksclient.auth_token) - - ks_kwargs['region_name'] = kwargs.get('os_region_name') - endpoint = (kwargs.get('watcher_url') or - _get_endpoint(_ksclient, **ks_kwargs)) - - auth_ref = _ksclient.auth_ref - - else: - e = (_('Must provide Keystone credentials or user-defined endpoint ' - 'and token')) - raise exc.AmbiguousAuthSystem(e) - - cli_kwargs = { - 'token': token, - 'insecure': kwargs.get('insecure'), - 'timeout': kwargs.get('timeout'), - 'ca_file': kwargs.get('ca_file'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), - 'auth_ref': auth_ref, + os_service_type = os_service_type or 'infra-optim' + os_endpoint_type = os_endpoint_type or 'publicURL' + project_id = (os_project_id or os_tenant_id) + project_name = (os_project_name or os_tenant_name) + kwargs = { + 'os_watcher_api_version': os_watcher_api_version, + 'max_retries': max_retries, + 'retry_interval': retry_interval, } + endpoint = watcher_url or os_endpoint_override + cacert = os_cacert or ca_file + cert = os_cert or cert_file + key = os_key or key_file + if os_auth_token and endpoint: + kwargs.update({ + 'token': os_auth_token, + 'insecure': insecure, + 'ca_file': cacert, + 'cert_file': cert, + 'key_file': key, + 'timeout': timeout, + }) + elif os_auth_url: + auth_type = 'password' + auth_kwargs = { + 'auth_url': os_auth_url, + 'project_id': project_id, + 'project_name': project_name, + 'user_domain_id': os_user_domain_id, + 'user_domain_name': os_user_domain_name, + 'project_domain_id': os_project_domain_id, + 'project_domain_name': os_project_domain_name, + } + if os_username and os_password: + auth_kwargs.update({ + 'username': os_username, + 'password': os_password, + }) + elif os_auth_token: + auth_type = 'token' + auth_kwargs.update({ + 'token': os_auth_token, + }) + # Create new session only if it was not passed in + if not session: + loader = kaloading.get_plugin_loader(auth_type) + auth_plugin = loader.load_from_options(**auth_kwargs) + # Let keystoneauth do the necessary parameter conversions + session = kaloading.session.Session().load_from_options( + auth=auth_plugin, insecure=insecure, cacert=cacert, + cert=cert, key=key, timeout=timeout, + ) - return Client(api_version, endpoint, **cli_kwargs) + exception_msg = _('Must provide Keystone credentials or user-defined ' + 'endpoint and token') + if not endpoint: + if session: + try: + # Pass the endpoint, it will be used to get hostname + # and port that will be used for API version caching. It will + # be also set as endpoint_override. + endpoint = session.get_endpoint( + service_type=os_service_type, + interface=os_endpoint_type, + region_name=os_region_name + ) + except Exception as e: + raise exceptions.AmbiguousAuthSystem( + exception_msg + _(', error was: %s') % e) + else: + # Neither session, nor valid auth parameters provided + raise exceptions.AmbiguousAuthSystem(exception_msg) + + # Always pass the session + kwargs['session'] = session + + return Client(api_version, endpoint, **kwargs) + + +def _get_client_class_and_version(version): + if not isinstance(version, api_versioning.APIVersion): + version = api_versioning.get_api_version(version) + else: + api_versioning.check_major_version(version) + if version.is_latest(): + raise exceptions.UnsupportedVersion( + _("The version should be explicit, not latest.")) + return version, importutils.import_class( + "watcherclient.v%s.client.Client" % version.ver_major) def Client(version, *args, **kwargs): - module = utils.import_versioned_module(version, 'client') - client_class = getattr(module, 'Client') + """Initialize client object based on given version. + + HOW-TO: + The simplest way to create a client instance is initialization with your + credentials:: + + >>> from watcherclient import client + >>> watcher = client.Client(VERSION, USERNAME, PASSWORD, + ... PROJECT_ID, AUTH_URL) + + Here ``VERSION`` can be a string or + ``watcherclient.api_versions.APIVersion`` obj. If you prefer string value, + you can use ``1`` or ``1.X`` (where X is a microversion). + + + Alternatively, you can create a client instance using the keystoneauth + session API. See "The watcherclient Python API" page at + python-watcherclient's doc. + """ + api_version, client_class = _get_client_class_and_version(version) return client_class(*args, **kwargs) diff --git a/watcherclient/common/api_versioning.py b/watcherclient/common/api_versioning.py new file mode 100644 index 0000000..50959d9 --- /dev/null +++ b/watcherclient/common/api_versioning.py @@ -0,0 +1,215 @@ +# +# 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 logging +import os +import pkgutil +import re + +from oslo_utils import strutils + +from watcherclient._i18n import _, _LW +from watcherclient import exceptions + +LOG = logging.getLogger(__name__) +if not LOG.handlers: + LOG.addHandler(logging.StreamHandler()) + + +HEADER_NAME = "OpenStack-API-Version" +SERVICE_TYPE = "infra-optim" +# key is a deprecated version and value is an alternative version. +DEPRECATED_VERSIONS = {} + +_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'") + + +class APIVersion(object): + """This class represents an API Version Request. + + This class provides convenience methods for manipulation + and comparison of version numbers that we need to do to + implement microversions. + """ + + def __init__(self, version_str=None): + """Create an API version object. + + :param version_str: String representation of APIVersionRequest. + Correct format is 'X.Y', where 'X' and 'Y' + are int values. None value should be used + to create Null APIVersionRequest, which is + equal to 0.0 + """ + self.ver_major = 0 + self.ver_minor = 0 + + if version_str is not None: + match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) + if match: + self.ver_major = int(match.group(1)) + if match.group(2) == "latest": + # NOTE(andreykurilin): Infinity allows to easily determine + # latest version and doesn't require any additional checks + # in comparison methods. + self.ver_minor = float("inf") + else: + self.ver_minor = int(match.group(2)) + else: + msg = _("Invalid format of client version '%s'. " + "Expected format 'X.Y', where X is a major part and Y " + "is a minor part of version.") % version_str + raise exceptions.UnsupportedVersion(msg) + + def __str__(self): + """Debug/Logging representation of object.""" + if self.is_latest(): + return "Latest API Version Major: %s" % self.ver_major + return ("API Version Major: %s, Minor: %s" + % (self.ver_major, self.ver_minor)) + + def __repr__(self): + if self.is_null(): + return "" + else: + return "" % self.get_string() + + def is_null(self): + return self.ver_major == 0 and self.ver_minor == 0 + + def is_latest(self): + return self.ver_minor == float("inf") + + def __lt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) < + (other.ver_major, other.ver_minor)) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) == + (other.ver_major, other.ver_minor)) + + def __gt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) > + (other.ver_major, other.ver_minor)) + + def __le__(self, other): + return self < other or self == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __ge__(self, other): + return self > other or self == other + + def matches(self, min_version, max_version): + """Matches the version object. + + Returns whether the version object represents a version + greater than or equal to the minimum version and less than + or equal to the maximum version. + + :param min_version: Minimum acceptable version. + :param max_version: Maximum acceptable version. + :returns: boolean + + If min_version is null then there is no minimum limit. + If max_version is null then there is no maximum limit. + If self is null then raise ValueError + """ + + if self.is_null(): + raise ValueError(_("Null APIVersion doesn't support 'matches'.")) + if max_version.is_null() and min_version.is_null(): + return True + elif max_version.is_null(): + return min_version <= self + elif min_version.is_null(): + return self <= max_version + else: + return min_version <= self <= max_version + + def get_string(self): + """Version string representation. + + Converts object to string representation which if used to create + an APIVersion object results in the same version. + """ + if self.is_null(): + raise ValueError( + _("Null APIVersion cannot be converted to string.")) + elif self.is_latest(): + return "%s.%s" % (self.ver_major, "latest") + return "%s.%s" % (self.ver_major, self.ver_minor) + + +def get_available_major_versions(): + # NOTE(andreykurilin): available clients version should not be + # hardcoded, so let's discover them. + matcher = re.compile(r"v[0-9]*$") + submodules = pkgutil.iter_modules( + [os.path.dirname(os.path.dirname(__file__))]) + available_versions = [name[1:] for loader, name, ispkg in submodules + if matcher.search(name)] + return available_versions + + +def check_major_version(api_version): + """Checks major part of ``APIVersion`` obj is supported. + + :raises watcherclient.exceptions.UnsupportedVersion: if major part is not + supported + """ + available_versions = get_available_major_versions() + if (not api_version.is_null() and + str(api_version.ver_major) not in available_versions): + if len(available_versions) == 1: + msg = _("Invalid client version '%(version)s'. " + "Major part should be '%(major)s'") % { + "version": api_version.get_string(), + "major": available_versions[0]} + else: + msg = _("Invalid client version '%(version)s'. " + "Major part must be one of: '%(major)s'") % { + "version": api_version.get_string(), + "major": ", ".join(available_versions)} + raise exceptions.UnsupportedVersion(msg) + + +def get_api_version(version_string): + """Returns checked APIVersion object""" + version_string = str(version_string) + if version_string in DEPRECATED_VERSIONS: + LOG.warning( + _LW("Version %(deprecated_version)s is deprecated, using " + "alternative version %(alternative)s instead."), + {"deprecated_version": version_string, + "alternative": DEPRECATED_VERSIONS[version_string]}) + version_string = DEPRECATED_VERSIONS[version_string] + if strutils.is_int_like(version_string): + version_string = "%s.0" % version_string + + api_version = APIVersion(version_string) + check_major_version(api_version) + return api_version diff --git a/watcherclient/common/apiclient/base.py b/watcherclient/common/apiclient/base.py index 436e43f..9059121 100644 --- a/watcherclient/common/apiclient/base.py +++ b/watcherclient/common/apiclient/base.py @@ -497,7 +497,7 @@ class Resource(object): def get(self): """Support for lazy loading details. - Some clients, such as novaclient have the option to lazy load the + Some clients, such as watcherclient have the option to lazy load the details, details which can be loaded with this function. """ # set_loaded() first ... so if we have to bail, we know we tried. diff --git a/watcherclient/common/apiclient/exceptions.py b/watcherclient/common/apiclient/exceptions.py index e36384f..f610051 100644 --- a/watcherclient/common/apiclient/exceptions.py +++ b/watcherclient/common/apiclient/exceptions.py @@ -433,11 +433,7 @@ def from_response(response, method, url): :param method: HTTP method used for request :param url: URL used for request """ - req_id = response.headers.get("x-openstack-request-id") - # NOTE(hdd) true for older versions of nova and cinder - if not req_id: - req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, diff --git a/watcherclient/common/http.py b/watcherclient/common/http.py deleted file mode 100644 index 85d7cd6..0000000 --- a/watcherclient/common/http.py +++ /dev/null @@ -1,388 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2012 OpenStack LLC. -# 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 copy -import json -import logging -import os -import socket -import ssl - -from keystoneclient import adapter -import six -import six.moves.urllib.parse as urlparse - -from watcherclient._i18n import _, _LW, _LE -from watcherclient import exceptions as exc - - -LOG = logging.getLogger(__name__) -USER_AGENT = 'python-watcherclient' -CHUNKSIZE = 1024 * 64 # 64kB - -API_VERSION = '/v1' - - -def _trim_endpoint_api_version(url): - """Trim API version and trailing slash from endpoint.""" - return url.rstrip('/').rstrip(API_VERSION) - - -def _extract_error_json(body): - """Return error_message from the HTTP response body.""" - error_json = {} - if six.PY3 and not isinstance(body, six.string_types): - body = body.decode("utf-8") - try: - body_json = json.loads(body) - if 'error_message' in body_json: - raw_msg = body_json['error_message'] - error_json = json.loads(raw_msg) - except ValueError: - pass - - return error_json - - -class HTTPClient(object): - - def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint - self.endpoint_trimmed = _trim_endpoint_api_version(endpoint) - self.auth_token = kwargs.get('token') - self.auth_ref = kwargs.get('auth_ref') - self.connection_params = self.get_connection_params(endpoint, **kwargs) - - @staticmethod - def get_connection_params(endpoint, **kwargs): - parts = urlparse.urlparse(endpoint) - - path = _trim_endpoint_api_version(parts.path) - - _args = (parts.hostname, parts.port, path) - _kwargs = {'timeout': (float(kwargs.get('timeout')) - if kwargs.get('timeout') else 600)} - - if parts.scheme == 'https': - _class = VerifiedHTTPSConnection - _kwargs['ca_file'] = kwargs.get('ca_file', None) - _kwargs['cert_file'] = kwargs.get('cert_file', None) - _kwargs['key_file'] = kwargs.get('key_file', None) - _kwargs['insecure'] = kwargs.get('insecure', False) - elif parts.scheme == 'http': - _class = six.moves.http_client.HTTPConnection - else: - raise exc.EndpointException( - _('Unsupported scheme: %s'), parts.scheme) - - return (_class, _args, _kwargs) - - def get_connection(self): - _class = self.connection_params[0] - try: - return _class(*self.connection_params[1][0:2], - **self.connection_params[2]) - except six.moves.http_client.InvalidURL: - raise exc.EndpointException() - - def log_curl_request(self, method, url, kwargs): - curl = ['curl -i -X %s' % method] - - for (key, value) in kwargs['headers'].items(): - header = '-H \'%s: %s\'' % (key, value) - curl.append(header) - - conn_params_fmt = [ - ('key_file', '--key %s'), - ('cert_file', '--cert %s'), - ('ca_file', '--cacert %s'), - ] - for (key, fmt) in conn_params_fmt: - value = self.connection_params[2].get(key) - if value: - curl.append(fmt % value) - - if self.connection_params[2].get('insecure'): - curl.append('-k') - - if 'body' in kwargs: - curl.append('-d \'%s\'' % kwargs['body']) - - curl.append(urlparse.urljoin(self.endpoint_trimmed, url)) - LOG.debug(' '.join(curl)) - - @staticmethod - def log_http_response(resp, body=None): - status = (resp.version / 10.0, resp.status, resp.reason) - dump = ['\nHTTP/%.1f %s %s' % status] - dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()]) - dump.append('') - if body: - dump.extend([body, '']) - LOG.debug('\n'.join(dump)) - - def _make_connection_url(self, url): - (_class, _args, _kwargs) = self.connection_params - base_url = _args[2] - return '%s/%s' % (base_url, url.lstrip('/')) - - def _http_request(self, url, method, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around httplib.HTTP(S)Connection.request to handle tasks such - as setting headers and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) - kwargs['headers'].setdefault('User-Agent', USER_AGENT) - if self.auth_token: - kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) - - self.log_curl_request(method, url, kwargs) - conn = self.get_connection() - - try: - conn_url = self._make_connection_url(url) - conn.request(method, conn_url, **kwargs) - resp = conn.getresponse() - except socket.gaierror as e: - raise exc.EndpointNotFound( - _("Error finding address for %(url)s: %(e)s"), - url=url, e=e) - except (socket.error, socket.timeout) as e: - endpoint = self.endpoint - raise exc.CommunicationError( - _("Error communicating with %(endpoint)s %(e)s"), - endpoint=endpoint, e=e) - body_iter = ResponseBodyIterator(resp) - - # Read body into string if it isn't obviously image data - body_str = None - if resp.getheader('content-type', None) != 'application/octet-stream': - body_str = ''.join([chunk for chunk in body_iter]) - self.log_http_response(resp, body_str) - body_iter = six.StringIO(body_str) - else: - self.log_http_response(resp) - - if 400 <= resp.status < 600: - LOG.warning(_LW("Request returned failure status.")) - error_json = _extract_error_json(body_str) - raise exc.from_response( - resp, error_json.get('faultstring'), - error_json.get('debuginfo'), method, url) - elif resp.status in (301, 302, 305): - # Redirected. Reissue the request to the new location. - return self._http_request(resp['location'], method, **kwargs) - elif resp.status == 300: - raise exc.from_response(resp, method=method, url=url) - - return resp, body_iter - - def json_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - kwargs['headers'].setdefault('Accept', 'application/json') - - if 'body' in kwargs: - kwargs['body'] = json.dumps(kwargs['body']) - - resp, body_iter = self._http_request(url, method, **kwargs) - content_type = resp.getheader('content-type', None) - - if resp.status == 204 or resp.status == 205 or content_type is None: - return resp, list() - - if 'application/json' in content_type: - body = ''.join([chunk for chunk in body_iter]) - try: - body = json.loads(body) - except ValueError: - LOG.error(_LE('Could not decode response body as JSON')) - else: - body = None - - return resp, body - - def raw_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - return self._http_request(url, method, **kwargs) - - -class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): - """httplib-compatibile connection using client-side SSL authentication - - :see http://code.activestate.com/recipes/ - 577548-https-httplib-client-connection-with-certificate-v/ - """ - - def __init__(self, host, port, key_file=None, cert_file=None, - ca_file=None, timeout=None, insecure=False): - six.moves.http_client.HTTPSConnection.__init__( - self, host, port, - key_file=key_file, - cert_file=cert_file) - self.key_file = key_file - self.cert_file = cert_file - if ca_file is not None: - self.ca_file = ca_file - else: - self.ca_file = self.get_system_ca_file() - self.timeout = timeout - self.insecure = insecure - - def connect(self): - """Connect to a host on a given (SSL) port. - - If ca_file is pointing somewhere, use it to check Server Certificate. - - Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). - This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to - ssl.wrap_socket(), which forces SSL to check server certificate against - our client certificate. - """ - sock = socket.create_connection((self.host, self.port), self.timeout) - - if self._tunnel_host: - self.sock = sock - self._tunnel() - - if self.insecure is True: - kwargs = {'cert_reqs': ssl.CERT_NONE} - else: - kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file} - - if self.cert_file: - kwargs['certfile'] = self.cert_file - if self.key_file: - kwargs['keyfile'] = self.key_file - - self.sock = ssl.wrap_socket(sock, **kwargs) - - @staticmethod - def get_system_ca_file(): - """Return path to system default CA file.""" - # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, - # Suse, FreeBSD/OpenBSD - ca_path = ['/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem', - '/etc/ssl/cert.pem'] - for ca in ca_path: - if os.path.exists(ca): - return ca - return None - - -class SessionClient(adapter.LegacyJsonAdapter): - """HTTP client based on Keystone client session.""" - - def _http_request(self, url, method, **kwargs): - kwargs.setdefault('user_agent', USER_AGENT) - kwargs.setdefault('auth', self.auth) - - endpoint_filter = kwargs.setdefault('endpoint_filter', {}) - endpoint_filter.setdefault('interface', self.interface) - endpoint_filter.setdefault('service_type', self.service_type) - endpoint_filter.setdefault('region_name', self.region_name) - - resp = self.session.request(url, method, - raise_exc=False, **kwargs) - - if 400 <= resp.status_code < 600: - error_json = _extract_error_json(resp.content) - raise exc.from_response(resp, error_json.get( - 'faultstring'), - error_json.get('debuginfo'), method, url) - elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location. - location = resp.headers.get('location') - resp = self._http_request(location, method, **kwargs) - elif resp.status_code == 300: - raise exc.from_response(resp, method=method, url=url) - return resp - - def json_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - kwargs['headers'].setdefault('Accept', 'application/json') - - if 'body' in kwargs: - kwargs['data'] = json.dumps(kwargs.pop('body')) - - resp = self._http_request(url, method, **kwargs) - body = resp.content - content_type = resp.headers.get('content-type', None) - status = resp.status_code - if status == 204 or status == 205 or content_type is None: - return resp, list() - if 'application/json' in content_type: - try: - body = resp.json() - except ValueError: - LOG.error(_LE('Could not decode response body as JSON')) - else: - body = None - - return resp, body - - def raw_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - return self._http_request(url, method, **kwargs) - - -class ResponseBodyIterator(object): - """A class that acts as an iterator over an HTTP response.""" - - def __init__(self, resp): - self.resp = resp - - def __iter__(self): - while True: - yield self.next() - - def next(self): - chunk = self.resp.read(CHUNKSIZE) - if chunk: - if six.PY3 and not isinstance(chunk, six.string_types): - chunk = chunk.decode("utf-8") - return chunk - else: - raise StopIteration() - - -def _construct_http_client(*args, **kwargs): - session = kwargs.pop('session', None) - auth = kwargs.pop('auth', None) - - if session: - service_type = kwargs.pop('service_type', 'infra-optim') - interface = kwargs.pop('endpoint_type', None) - region_name = kwargs.pop('region_name', None) - return SessionClient(session=session, - auth=auth, - interface=interface, - service_type=service_type, - region_name=region_name, - service_name=None, - user_agent='python-watcherclient') - else: - return HTTPClient(*args, **kwargs) diff --git a/watcherclient/common/httpclient.py b/watcherclient/common/httpclient.py new file mode 100644 index 0000000..7df2326 --- /dev/null +++ b/watcherclient/common/httpclient.py @@ -0,0 +1,632 @@ +# Copyright 2012 OpenStack LLC. +# 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 copy +from distutils import version +import functools +import hashlib +import json +import logging +import os +import socket +import ssl +import textwrap +import time + +from keystoneauth1 import adapter +from keystoneauth1 import exceptions as kexceptions +from oslo_utils import strutils +import requests +import six +from six.moves import http_client +import six.moves.urllib.parse as urlparse + +from watcherclient._i18n import _, _LE, _LW +from watcherclient import exceptions + + +# NOTE(deva): Record the latest version that this client was tested with. +# We still have a lot of work to do in the client to implement +# microversion support in the client properly! See +# http://specs.openstack.org/openstack/watcher-specs/specs/kilo/api-microversions.html # noqa +# for full details. +DEFAULT_VER = '1.0' + + +LOG = logging.getLogger(__name__) +USER_AGENT = 'python-watcherclient' +CHUNKSIZE = 1024 * 64 # 64kB + +API_VERSION = '/v1' +API_VERSION_SELECTED_STATES = ('user', 'negotiated', 'cached', 'default') + + +DEFAULT_MAX_RETRIES = 5 +DEFAULT_RETRY_INTERVAL = 2 +SENSITIVE_HEADERS = ('X-Auth-Token',) + + +SUPPORTED_ENDPOINT_SCHEME = ('http', 'https') + + +def _trim_endpoint_api_version(url): + """Trim API version and trailing slash from endpoint.""" + return url.rstrip('/').rstrip(API_VERSION) + + +def _extract_error_json(body): + """Return error_message from the HTTP response body.""" + error_json = {} + try: + body_json = json.loads(body) + if 'error_message' in body_json: + raw_msg = body_json['error_message'] + error_json = json.loads(raw_msg) + except ValueError: + pass + + return error_json + + +def get_server(endpoint): + """Extract and return the server & port that we're connecting to.""" + if endpoint is None: + return None, None + parts = urlparse.urlparse(endpoint) + return parts.hostname, str(parts.port) + + +class VersionNegotiationMixin(object): + def negotiate_version(self, conn, resp): + """Negotiate the server version + + Assumption: Called after receiving a 406 error when doing a request. + + param conn: A connection object + param resp: The response object from http request + """ + if self.api_version_select_state not in API_VERSION_SELECTED_STATES: + raise RuntimeError( + _('Error: self.api_version_select_state should be one of the ' + 'values in: "%(valid)s" but had the value: "%(value)s"') % + {'valid': ', '.join(API_VERSION_SELECTED_STATES), + 'value': self.api_version_select_state}) + min_ver, max_ver = self._parse_version_headers(resp) + # NOTE: servers before commit 32fb6e99 did not return version headers + # on error, so we need to perform a GET to determine + # the supported version range + if not max_ver: + LOG.debug('No version header in response, requesting from server') + if self.os_watcher_api_version: + base_version = ("/v%s" % + str(self.os_watcher_api_version).split('.')[0]) + else: + base_version = API_VERSION + resp = self._make_simple_request(conn, 'GET', base_version) + min_ver, max_ver = self._parse_version_headers(resp) + # If the user requested an explicit version or we have negotiated a + # version and still failing then error now. The server could + # support the version requested but the requested operation may not + # be supported by the requested version. + if self.api_version_select_state == 'user': + raise exceptions.UnsupportedVersion(textwrap.fill( + _("Requested API version %(req)s is not supported by the " + "server or the requested operation is not supported by the " + "requested version. Supported version range is %(min)s to " + "%(max)s") + % {'req': self.os_watcher_api_version, + 'min': min_ver, 'max': max_ver})) + if self.api_version_select_state == 'negotiated': + raise exceptions.UnsupportedVersion(textwrap.fill( + _("No API version was specified and the requested operation " + "was not supported by the client's negotiated API version " + "%(req)s. Supported version range is: %(min)s to %(max)s") + % {'req': self.os_watcher_api_version, + 'min': min_ver, 'max': max_ver})) + + negotiated_ver = str( + min(version.StrictVersion(self.os_watcher_api_version), + version.StrictVersion(max_ver))) + if negotiated_ver < min_ver: + negotiated_ver = min_ver + # server handles microversions, but doesn't support + # the requested version, so try a negotiated version + self.api_version_select_state = 'negotiated' + self.os_watcher_api_version = negotiated_ver + LOG.debug('Negotiated API version is %s', negotiated_ver) + + return negotiated_ver + + def _generic_parse_version_headers(self, accessor_func): + min_ver = accessor_func('X-OpenStack-Watcher-API-Minimum-Version', + None) + max_ver = accessor_func('X-OpenStack-Watcher-API-Maximum-Version', + None) + return min_ver, max_ver + + def _parse_version_headers(self, accessor_func): + # NOTE(jlvillal): Declared for unit testing purposes + raise NotImplementedError() + + def _make_simple_request(self, conn, method, url): + # NOTE(jlvillal): Declared for unit testing purposes + raise NotImplementedError() + + +_RETRY_EXCEPTIONS = (exceptions.Conflict, + exceptions.ServiceUnavailable, + exceptions.ConnectionRefused, + kexceptions.RetriableConnectionFailure) + + +def with_retries(func): + """Wrapper for _http_request adding support for retries.""" + @functools.wraps(func) + def wrapper(self, url, method, **kwargs): + if self.conflict_max_retries is None: + self.conflict_max_retries = DEFAULT_MAX_RETRIES + if self.conflict_retry_interval is None: + self.conflict_retry_interval = DEFAULT_RETRY_INTERVAL + + num_attempts = self.conflict_max_retries + 1 + for attempt in range(1, num_attempts + 1): + try: + return func(self, url, method, **kwargs) + except _RETRY_EXCEPTIONS as error: + msg = (_LE("Error contacting Watcher server: %(error)s. " + "Attempt %(attempt)d of %(total)d") % + {'attempt': attempt, + 'total': num_attempts, + 'error': error}) + if attempt == num_attempts: + LOG.error(msg) + raise + else: + LOG.debug(msg) + time.sleep(self.conflict_retry_interval) + + return wrapper + + +class HTTPClient(VersionNegotiationMixin): + + def __init__(self, endpoint, **kwargs): + self.endpoint = endpoint + self.endpoint_trimmed = _trim_endpoint_api_version(endpoint) + self.auth_token = kwargs.get('token') + self.auth_ref = kwargs.get('auth_ref') + self.os_watcher_api_version = kwargs.get('os_watcher_api_version', + DEFAULT_VER) + self.api_version_select_state = kwargs.get( + 'api_version_select_state', 'default') + self.conflict_max_retries = kwargs.pop('max_retries', + DEFAULT_MAX_RETRIES) + self.conflict_retry_interval = kwargs.pop('retry_interval', + DEFAULT_RETRY_INTERVAL) + self.session = requests.Session() + + parts = urlparse.urlparse(endpoint) + if parts.scheme not in SUPPORTED_ENDPOINT_SCHEME: + msg = _('Unsupported scheme: %s') % parts.scheme + raise exceptions.EndpointException(msg) + + if parts.scheme == 'https': + if kwargs.get('insecure') is True: + self.session.verify = False + elif kwargs.get('ca_file'): + self.session.verify = kwargs['ca_file'] + self.session.cert = (kwargs.get('cert_file'), + kwargs.get('key_file')) + + def _process_header(self, name, value): + """Redacts any sensitive header + + Redact a header that contains sensitive information, by returning an + updated header with the sha1 hash of that value. The redacted value is + prefixed by '{SHA1}' because that's the convention used within + OpenStack. + + :returns: A tuple of (name, value) + name: the safe encoding format of name + value: the redacted value if name is x-auth-token, + or the safe encoding format of name + + """ + if name in SENSITIVE_HEADERS: + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return (name, "{SHA1}%s" % d) + else: + return (name, value) + + def log_curl_request(self, method, url, kwargs): + curl = ['curl -i -X %s' % method] + + for (key, value) in kwargs['headers'].items(): + header = '-H \'%s: %s\'' % self._process_header(key, value) + curl.append(header) + + if not self.session.verify: + curl.append('-k') + elif isinstance(self.session.verify, six.string_types): + curl.append('--cacert %s' % self.session.verify) + + if self.session.cert: + curl.append('--cert %s' % self.session.cert[0]) + curl.append('--key %s' % self.session.cert[1]) + + if 'body' in kwargs: + body = strutils.mask_password(kwargs['body']) + curl.append('-d \'%s\'' % body) + + curl.append(urlparse.urljoin(self.endpoint_trimmed, url)) + LOG.debug(' '.join(curl)) + + @staticmethod + def log_http_response(resp, body=None): + # NOTE(aarefiev): resp.raw is urllib3 response object, it's used + # only to get 'version', response from request with 'stream = True' + # should be used for raw reading. + status = (resp.raw.version / 10.0, resp.status_code, resp.reason) + dump = ['\nHTTP/%.1f %s %s' % status] + dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) + dump.append('') + if body: + body = strutils.mask_password(body) + dump.extend([body, '']) + LOG.debug('\n'.join(dump)) + + def _make_connection_url(self, url): + return urlparse.urljoin(self.endpoint_trimmed, url) + + def _parse_version_headers(self, resp): + return self._generic_parse_version_headers(resp.headers.get) + + def _make_simple_request(self, conn, method, url): + return conn.request(method, self._make_connection_url(url)) + + @with_retries + def _http_request(self, url, method, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around request.Session.request to handle tasks such + as setting headers and error handling. + """ + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', USER_AGENT) + if self.os_watcher_api_version: + kwargs['headers'].setdefault('X-OpenStack-Watcher-API-Version', + self.os_watcher_api_version) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + + self.log_curl_request(method, url, kwargs) + + # NOTE(aarefiev): This is for backwards compatibility, request + # expected body in 'data' field, previously we used httplib, + # which expected 'body' field. + body = kwargs.pop('body', None) + if body: + kwargs['data'] = body + + conn_url = self._make_connection_url(url) + try: + resp = self.session.request(method, + conn_url, + **kwargs) + + # TODO(deva): implement graceful client downgrade when connecting + # to servers that did not support microversions. Details here: + # http://specs.openstack.org/openstack/watcher-specs/specs/kilo/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-watcher-user-specified # noqa + + if resp.status_code == http_client.NOT_ACCEPTABLE: + negotiated_ver = self.negotiate_version(self.session, resp) + kwargs['headers']['X-OpenStack-Watcher-API-Version'] = ( + negotiated_ver) + return self._http_request(url, method, **kwargs) + + except requests.exceptions.RequestException as e: + message = (_("Error has occurred while handling " + "request for %(url)s: %(e)s") % + dict(url=conn_url, e=e)) + # NOTE(aarefiev): not valid request(invalid url, missing schema, + # and so on), retrying is not needed. + if isinstance(e, ValueError): + raise exceptions.ValidationError(message) + + raise exceptions.ConnectionRefused(message) + + body_iter = resp.iter_content(chunk_size=CHUNKSIZE) + + # Read body into string if it isn't obviously image data + body_str = None + if resp.headers.get('Content-Type') != 'application/octet-stream': + body_str = ''.join([chunk for chunk in body_iter]) + self.log_http_response(resp, body_str) + body_iter = six.StringIO(body_str) + else: + self.log_http_response(resp) + + if resp.status_code >= http_client.BAD_REQUEST: + error_json = _extract_error_json(body_str) + raise exceptions.from_response( + resp, error_json.get('faultstring'), + error_json.get('debuginfo'), method, url) + elif resp.status_code in (http_client.MOVED_PERMANENTLY, + http_client.FOUND, + http_client.USE_PROXY): + # Redirected. Reissue the request to the new location. + return self._http_request(resp['location'], method, **kwargs) + elif resp.status_code == http_client.MULTIPLE_CHOICES: + raise exceptions.from_response(resp, method=method, url=url) + + return resp, body_iter + + def json_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + kwargs['headers'].setdefault('Accept', 'application/json') + + if 'body' in kwargs: + kwargs['body'] = json.dumps(kwargs['body']) + + resp, body_iter = self._http_request(url, method, **kwargs) + content_type = resp.headers.get('Content-Type') + + if (resp.status_code in (http_client.NO_CONTENT, + http_client.RESET_CONTENT) + or content_type is None): + return resp, list() + + if 'application/json' in content_type: + body = ''.join([chunk for chunk in body_iter]) + try: + body = json.loads(body) + except ValueError: + LOG.error(_LE('Could not decode response body as JSON')) + else: + body = None + + return resp, body + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + return self._http_request(url, method, **kwargs) + + +class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): + """httplib-compatible connection using client-side SSL authentication + + :see http://code.activestate.com/recipes/ + 577548-https-httplib-client-connection-with-certificate-v/ + """ + + def __init__(self, host, port, key_file=None, cert_file=None, + ca_file=None, timeout=None, insecure=False): + six.moves.http_client.HTTPSConnection.__init__(self, host, port, + key_file=key_file, + cert_file=cert_file) + self.key_file = key_file + self.cert_file = cert_file + if ca_file is not None: + self.ca_file = ca_file + else: + self.ca_file = self.get_system_ca_file() + self.timeout = timeout + self.insecure = insecure + + def connect(self): + """Connect to a host on a given (SSL) port. + + If ca_file is pointing somewhere, use it to check Server Certificate. + + Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). + This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to + ssl.wrap_socket(), which forces SSL to check server certificate against + our client certificate. + """ + sock = socket.create_connection((self.host, self.port), self.timeout) + + if self._tunnel_host: + self.sock = sock + self._tunnel() + + if self.insecure is True: + kwargs = {'cert_reqs': ssl.CERT_NONE} + else: + kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file} + + if self.cert_file: + kwargs['certfile'] = self.cert_file + if self.key_file: + kwargs['keyfile'] = self.key_file + + self.sock = ssl.wrap_socket(sock, **kwargs) + + @staticmethod + def get_system_ca_file(): + """Return path to system default CA file.""" + # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, + # Suse, FreeBSD/OpenBSD + ca_path = ['/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem', + '/etc/ssl/cert.pem'] + for ca in ca_path: + if os.path.exists(ca): + return ca + return None + + +class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter): + """HTTP client based on Keystone client session.""" + + def __init__(self, + os_watcher_api_version, + api_version_select_state, + max_retries, + retry_interval, + endpoint, + **kwargs): + self.os_watcher_api_version = os_watcher_api_version + self.api_version_select_state = api_version_select_state + self.conflict_max_retries = max_retries + self.conflict_retry_interval = retry_interval + self.endpoint = endpoint + + super(SessionClient, self).__init__(**kwargs) + + def _parse_version_headers(self, resp): + return self._generic_parse_version_headers(resp.headers.get) + + def _make_simple_request(self, conn, method, url): + # NOTE: conn is self.session for this class + return conn.request(url, method, raise_exc=False) + + @with_retries + def _http_request(self, url, method, **kwargs): + kwargs.setdefault('user_agent', USER_AGENT) + kwargs.setdefault('auth', self.auth) + if isinstance(self.endpoint_override, six.string_types): + kwargs.setdefault( + 'endpoint_override', + _trim_endpoint_api_version(self.endpoint_override) + ) + + if getattr(self, 'os_watcher_api_version', None): + kwargs['headers'].setdefault('X-OpenStack-Watcher-API-Version', + self.os_watcher_api_version) + + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('interface', self.interface) + endpoint_filter.setdefault('service_type', self.service_type) + endpoint_filter.setdefault('region_name', self.region_name) + + resp = self.session.request(url, method, + raise_exc=False, **kwargs) + if resp.status_code == http_client.NOT_ACCEPTABLE: + negotiated_ver = self.negotiate_version(self.session, resp) + kwargs['headers']['X-OpenStack-Watcher-API-Version'] = ( + negotiated_ver) + return self._http_request(url, method, **kwargs) + if resp.status_code >= http_client.BAD_REQUEST: + error_json = _extract_error_json(resp.content) + raise exceptions.from_response( + resp, error_json.get('faultstring'), + error_json.get('debuginfo'), method, url) + elif resp.status_code in (http_client.MOVED_PERMANENTLY, + http_client.FOUND, http_client.USE_PROXY): + # Redirected. Reissue the request to the new location. + location = resp.headers.get('location') + resp = self._http_request(location, method, **kwargs) + elif resp.status_code == http_client.MULTIPLE_CHOICES: + raise exceptions.from_response(resp, method=method, url=url) + return resp + + def json_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + kwargs['headers'].setdefault('Accept', 'application/json') + + if 'body' in kwargs: + kwargs['data'] = json.dumps(kwargs.pop('body')) + + resp = self._http_request(url, method, **kwargs) + body = resp.content + content_type = resp.headers.get('content-type', None) + status = resp.status_code + if (status in (http_client.NO_CONTENT, http_client.RESET_CONTENT) or + content_type is None): + return resp, list() + if 'application/json' in content_type: + try: + body = resp.json() + except ValueError: + LOG.error(_LE('Could not decode response body as JSON')) + else: + body = None + + return resp, body + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + return self._http_request(url, method, **kwargs) + + +def _construct_http_client(endpoint=None, + session=None, + token=None, + auth_ref=None, + os_watcher_api_version=DEFAULT_VER, + api_version_select_state='default', + max_retries=DEFAULT_MAX_RETRIES, + retry_interval=DEFAULT_RETRY_INTERVAL, + timeout=600, + ca_file=None, + cert_file=None, + key_file=None, + insecure=None, + **kwargs): + if session: + kwargs.setdefault('service_type', 'infra-optim') + kwargs.setdefault('user_agent', 'python-watcherclient') + kwargs.setdefault('interface', kwargs.pop('endpoint_type', None)) + kwargs.setdefault('endpoint_override', endpoint) + + ignored = {'token': token, + 'auth_ref': auth_ref, + 'timeout': timeout != 600, + 'ca_file': ca_file, + 'cert_file': cert_file, + 'key_file': key_file, + 'insecure': insecure} + + dvars = [k for k, v in ignored.items() if v] + + if dvars: + LOG.warning(_LW('The following arguments are ignored when using ' + 'the session to construct a client: %s'), + ', '.join(dvars)) + + return SessionClient(session=session, + os_watcher_api_version=os_watcher_api_version, + api_version_select_state=api_version_select_state, + max_retries=max_retries, + retry_interval=retry_interval, + endpoint=endpoint, + **kwargs) + else: + if kwargs: + LOG.warning(_LW('The following arguments are being ignored when ' + 'constructing the client: %s'), ', '.join(kwargs)) + + return HTTPClient(endpoint=endpoint, + token=token, + auth_ref=auth_ref, + os_watcher_api_version=os_watcher_api_version, + api_version_select_state=api_version_select_state, + max_retries=max_retries, + retry_interval=retry_interval, + timeout=timeout, + ca_file=ca_file, + cert_file=cert_file, + key_file=key_file, + insecure=insecure) diff --git a/watcherclient/exceptions.py b/watcherclient/exceptions.py index 241e857..8c622da 100644 --- a/watcherclient/exceptions.py +++ b/watcherclient/exceptions.py @@ -53,6 +53,20 @@ A generic error message, given when no more specific message is suitable. An alias of :py:exc:`watcherclient.common.apiclient.ValidationError` """ +Conflict = exceptions.Conflict +ConnectionRefused = exceptions.ConnectionRefused +EndpointException = exceptions.EndpointException +EndpointNotFound = exceptions.EndpointNotFound +ServiceUnavailable = exceptions.ServiceUnavailable + + +class UnsupportedVersion(Exception): + """Unsupported API Version + + Indicates that the user is trying to use an unsupported version of the API. + """ + pass + class AmbiguousAuthSystem(exceptions.ClientException): """Could not obtain token and endpoint using provided credentials.""" @@ -86,11 +100,10 @@ def from_response(response, message=None, traceback=None, method=None, 'Content-Type': response.getheader('content-type', "")} if hasattr(response, 'status_code'): - # NOTE(jiangfei): These modifications allow SessionClient - # to handle faultstring. + # NOTE(hongbin): This allows SessionClient to handle faultstring. response.json = lambda: {'error': error_body} - if (response.headers['Content-Type'].startswith('text/') and + if (response.headers.get('Content-Type', '').startswith('text/') and not hasattr(response, 'text')): # NOTE(clif_h): There seems to be a case in the # common.apiclient.exceptions module where if the @@ -100,4 +113,4 @@ def from_response(response, message=None, traceback=None, method=None, # This is to work around that problem. response.text = '' - return exceptions.from_response(response, message, url) + return exceptions.from_response(response, method, url) diff --git a/watcherclient/plugin.py b/watcherclient/plugin.py index 330654d..5885de3 100644 --- a/watcherclient/plugin.py +++ b/watcherclient/plugin.py @@ -26,32 +26,16 @@ API_VERSIONS = { def make_client(instance): - """Returns an infra-optim service client""" - watcher_client = utils.get_client_class( + """Returns an infra-optim service client.""" + infraoptim_client_class = utils.get_client_class( API_NAME, instance._api_version[API_NAME], API_VERSIONS) - LOG.debug('Instantiating infra-optim client: %s', watcher_client) + LOG.debug('Instantiating infraoptim client: %s', infraoptim_client_class) - endpoint = instance.get_endpoint_for_service_type( - API_NAME, - region_name=instance._region_name, - interface=instance._interface, - ) - - auth_url = instance._auth_url \ - if hasattr(instance, '_auth_url') else instance.auth.auth_url - username = instance._username \ - if hasattr(instance, '_username') else instance.auth._username - password = instance._password \ - if hasattr(instance, '_password') else instance.auth._password - - client = watcher_client( - endpoint=endpoint, + client = infraoptim_client_class( + os_watcher_api_version=instance._api_version[API_NAME], session=instance.session, - auth_url=auth_url, - username=username, - password=password, region_name=instance._region_name, ) diff --git a/watcherclient/shell.py b/watcherclient/shell.py index 198c788..2f631ab 100644 --- a/watcherclient/shell.py +++ b/watcherclient/shell.py @@ -25,17 +25,11 @@ from cliff import command from cliff import commandmanager from cliff import complete from cliff import help as cli_help -from keystoneclient.auth.identity import v2 -from keystoneclient.auth.identity import v3 -from keystoneclient import discover -from keystoneclient import exceptions as ks_exc from keystoneclient import session from osc_lib import logs from osc_lib import utils -import six.moves.urllib.parse as urlparse -from watcherclient._i18n import _ -from watcherclient import exceptions as exc +from watcherclient import client as watcherclient from watcherclient import version LOG = logging.getLogger(__name__) @@ -75,145 +69,9 @@ class WatcherShell(app.App): ) def create_client(self, args): - service_type = 'infra-optim' - project_id = args.os_project_id or args.os_tenant_id - project_name = args.os_project_name or args.os_tenant_name - - keystone_session = session.Session.load_from_cli_options(args) - - kwargs = { - 'username': args.os_username, - 'user_domain_id': args.os_user_domain_id, - 'user_domain_name': args.os_user_domain_name, - 'password': args.os_password, - 'auth_token': args.os_auth_token, - 'project_id': project_id, - 'project_name': project_name, - 'project_domain_id': args.os_project_domain_id, - 'project_domain_name': args.os_project_domain_name, - } - keystone_auth = self._get_keystone_auth(keystone_session, - args.os_auth_url, - **kwargs) - region_name = args.os_region_name - endpoint = keystone_auth.get_endpoint(keystone_session, - service_type=service_type, - region_name=region_name) - - endpoint_type = args.os_endpoint_type or 'publicURL' - kwargs = { - 'auth_url': args.os_auth_url, - 'session': keystone_session, - 'auth': keystone_auth, - 'service_type': service_type, - 'endpoint_type': endpoint_type, - 'region_name': args.os_region_name, - 'username': args.os_username, - 'password': args.os_password, - } - - watcher_client = utils.get_client_class( - API_NAME, - args.watcher_api_version or 1, - API_VERSIONS) - LOG.debug('Instantiating infra-optim client: %s', watcher_client) - - client = watcher_client(args.watcher_api_version, endpoint, **kwargs) - + client = watcherclient.get_client('1', **args.__dict__) return client - def _discover_auth_versions(self, session, auth_url): - # discover the API versions the server is supporting base on the - # given URL - v2_auth_url = None - v3_auth_url = None - try: - ks_discover = discover.Discover(session=session, auth_url=auth_url) - v2_auth_url = ks_discover.url_for('2.0') - v3_auth_url = ks_discover.url_for('3.0') - except ks_exc.ClientException: - # Identity service may not support discover API version. - # Let's try to figure out the API version from the original URL. - url_parts = urlparse.urlparse(auth_url) - (scheme, netloc, path, params, query, fragment) = url_parts - path = path.lower() - if path.startswith('/v3'): - v3_auth_url = auth_url - elif path.startswith('/v2'): - v2_auth_url = auth_url - else: - # not enough information to determine the auth version - msg = _('Unable to determine the Keystone version ' - 'to authenticate with using the given ' - 'auth_url. Identity service may not support API ' - 'version discovery. Please provide a versioned ' - 'auth_url instead. %s') % auth_url - raise exc.CommandError(msg) - - return (v2_auth_url, v3_auth_url) - - def _get_keystone_v3_auth(self, v3_auth_url, **kwargs): - auth_token = kwargs.pop('auth_token', None) - if auth_token: - return v3.Token(v3_auth_url, auth_token) - else: - return v3.Password(v3_auth_url, **kwargs) - - def _get_keystone_v2_auth(self, v2_auth_url, **kwargs): - auth_token = kwargs.pop('auth_token', None) - if auth_token: - return v2.Token( - v2_auth_url, - auth_token, - tenant_id=kwargs.pop('project_id', None), - tenant_name=kwargs.pop('project_name', None)) - else: - return v2.Password( - v2_auth_url, - username=kwargs.pop('username', None), - password=kwargs.pop('password', None), - tenant_id=kwargs.pop('project_id', None), - tenant_name=kwargs.pop('project_name', None)) - - def _get_keystone_auth(self, session, auth_url, **kwargs): - # FIXME(dhu): this code should come from keystoneclient - - # discover the supported keystone versions using the given url - (v2_auth_url, v3_auth_url) = self._discover_auth_versions( - session=session, - auth_url=auth_url) - - # Determine which authentication plugin to use. First inspect the - # auth_url to see the supported version. If both v3 and v2 are - # supported, then use the highest version if possible. - auth = None - if v3_auth_url and v2_auth_url: - user_domain_name = kwargs.get('user_domain_name', None) - user_domain_id = kwargs.get('user_domain_id', None) - project_domain_name = kwargs.get('project_domain_name', None) - project_domain_id = kwargs.get('project_domain_id', None) - - # support both v2 and v3 auth. Use v3 if domain information is - # provided. - if (user_domain_name or user_domain_id or project_domain_name or - project_domain_id): - auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs) - else: - auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs) - elif v3_auth_url: - # support only v3 - auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs) - elif v2_auth_url: - # support only v2 - auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs) - else: - raise exc.CommandError( - _('Unable to determine the Keystone version ' - 'to authenticate with using the given ' - 'auth_url.')) - - return auth - def build_option_parser(self, description, version, argparse_kwargs=None): """Introduces global arguments for the application. @@ -291,14 +149,20 @@ class WatcherShell(app.App): metavar='', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN].') - parser.add_argument('--watcher-api-version', - metavar='', - default=utils.env('WATCHER_API_VERSION'), - help='Defaults to env[WATCHER_API_VERSION].') + parser.add_argument('--os-watcher-api-version', + metavar='', + default=utils.env('OS_WATCHER_API_VERSION', + default='1'), + help='Defaults to env[OS_WATCHER_API_VERSION].') parser.add_argument('--os-endpoint-type', default=utils.env('OS_ENDPOINT_TYPE'), help='Defaults to env[OS_ENDPOINT_TYPE] or ' '"publicURL"') + parser.add_argument('--os-endpoint-override', + metavar='', + default=utils.env('OS_ENDPOINT_OVERRIDE'), + help="Use this API endpoint instead of the " + "Service Catalog.") parser.epilog = ('See "watcher help COMMAND" for help ' 'on a specific command.') session.Session.register_cli_options(parser) diff --git a/watcherclient/tests/common/__init__.py b/watcherclient/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watcherclient/tests/common/test_api_versioning.py b/watcherclient/tests/common/test_api_versioning.py new file mode 100644 index 0000000..29b42a7 --- /dev/null +++ b/watcherclient/tests/common/test_api_versioning.py @@ -0,0 +1,150 @@ +# Copyright 2016 Mirantis +# 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 mock + +from watcherclient.common import api_versioning +from watcherclient import exceptions +from watcherclient.tests import utils + + +class APIVersionTestCase(utils.BaseTestCase): + def test_valid_version_strings(self): + def _test_string(version, exp_major, exp_minor): + v = api_versioning.APIVersion(version) + self.assertEqual(v.ver_major, exp_major) + self.assertEqual(v.ver_minor, exp_minor) + + _test_string("1.1", 1, 1) + _test_string("2.10", 2, 10) + _test_string("5.234", 5, 234) + _test_string("12.5", 12, 5) + _test_string("2.0", 2, 0) + _test_string("2.200", 2, 200) + + def test_null_version(self): + v = api_versioning.APIVersion() + self.assertTrue(v.is_null()) + + def test_invalid_version_strings(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "2") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "200") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "2.1.4") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "200.23.66.3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "5 .3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "5. 3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "5.03") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "02.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "2.001") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, " 2.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.APIVersion, "2.1 ") + + def test_version_comparisons(self): + v1 = api_versioning.APIVersion("2.0") + v2 = api_versioning.APIVersion("2.5") + v3 = api_versioning.APIVersion("5.23") + v4 = api_versioning.APIVersion("2.0") + v_null = api_versioning.APIVersion() + + self.assertTrue(v1 < v2) + self.assertTrue(v3 > v2) + self.assertTrue(v1 != v2) + self.assertTrue(v1 == v4) + self.assertTrue(v1 != v_null) + self.assertTrue(v_null == v_null) + self.assertRaises(TypeError, v1.__le__, "2.1") + + def test_version_matches(self): + v1 = api_versioning.APIVersion("2.0") + v2 = api_versioning.APIVersion("2.5") + v3 = api_versioning.APIVersion("2.45") + v4 = api_versioning.APIVersion("3.3") + v5 = api_versioning.APIVersion("3.23") + v6 = api_versioning.APIVersion("2.0") + v7 = api_versioning.APIVersion("3.3") + v8 = api_versioning.APIVersion("4.0") + v_null = api_versioning.APIVersion() + + self.assertTrue(v2.matches(v1, v3)) + self.assertTrue(v2.matches(v1, v_null)) + self.assertTrue(v1.matches(v6, v2)) + self.assertTrue(v4.matches(v2, v7)) + self.assertTrue(v4.matches(v_null, v7)) + self.assertTrue(v4.matches(v_null, v8)) + self.assertFalse(v1.matches(v2, v3)) + self.assertFalse(v5.matches(v2, v4)) + self.assertFalse(v2.matches(v3, v1)) + + self.assertRaises(ValueError, v_null.matches, v1, v3) + + def test_get_string(self): + v1_string = "3.23" + v1 = api_versioning.APIVersion(v1_string) + self.assertEqual(v1_string, v1.get_string()) + + self.assertRaises(ValueError, + api_versioning.APIVersion().get_string) + + +class GetAPIVersionTestCase(utils.BaseTestCase): + def test_get_available_client_versions(self): + output = api_versioning.get_available_major_versions() + self.assertNotEqual([], output) + + def test_wrong_format(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.get_api_version, "something_wrong") + + def test_wrong_major_version(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versioning.get_api_version, "2") + + @mock.patch("watcherclient.common.api_versioning.APIVersion") + def test_only_major_part_is_presented(self, mock_apiversion): + version = 7 + self.assertEqual(mock_apiversion.return_value, + api_versioning.get_api_version(version)) + mock_apiversion.assert_called_once_with("%s.0" % str(version)) + + @mock.patch("watcherclient.common.api_versioning.APIVersion") + def test_major_and_minor_parts_is_presented(self, mock_apiversion): + version = "2.7" + self.assertEqual(mock_apiversion.return_value, + api_versioning.get_api_version(version)) + mock_apiversion.assert_called_once_with(version) diff --git a/watcherclient/tests/test_client.py b/watcherclient/tests/test_client.py index 82a0267..d824713 100644 --- a/watcherclient/tests/test_client.py +++ b/watcherclient/tests/test_client.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # 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 @@ -12,35 +10,50 @@ # License for the specific language governing permissions and limitations # under the License. -import fixtures +import mock -from watcherclient.client import get_client -from watcherclient import exceptions as exc +from keystoneauth1 import loading as kaloading + +from watcherclient import client as watcherclient +from watcherclient.common import httpclient +from watcherclient import exceptions from watcherclient.tests import utils -def fake_get_ksclient(**kwargs): - return utils.FakeKeystone('KSCLIENT_AUTH_TOKEN') - - class ClientTest(utils.BaseTestCase): def test_get_client_with_auth_token_watcher_url(self): - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', fake_get_ksclient)) kwargs = { - 'watcher_url': 'http://watcher.example.org:6385/', + 'watcher_url': 'http://watcher.example.org:9322/', 'os_auth_token': 'USER_AUTH_TOKEN', } - client = get_client('1', **kwargs) + client = watcherclient.get_client('1', **kwargs) self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token) - self.assertEqual('http://watcher.example.org:6385/', + self.assertEqual('http://watcher.example.org:9322/', client.http_client.endpoint) + @mock.patch.object(kaloading.session, 'Session', autospec=True) + @mock.patch.object(kaloading, 'get_plugin_loader', autospec=True) + def _test_get_client(self, mock_ks_loader, mock_ks_session, + version=None, auth='password', **kwargs): + session = mock_ks_session.return_value.load_from_options.return_value + session.get_endpoint.return_value = 'http://localhost:9322/v1/f14b4123' + mock_ks_loader.return_value.load_from_options.return_value = 'auth' + + watcherclient.get_client('1', **kwargs) + + mock_ks_loader.assert_called_once_with(auth) + mock_ks_session.return_value.load_from_options.assert_called_once_with( + auth='auth', timeout=kwargs.get('timeout'), + insecure=kwargs.get('insecure'), cert=kwargs.get('cert'), + cacert=kwargs.get('cacert'), key=kwargs.get('key')) + session.get_endpoint.assert_called_once_with( + service_type=kwargs.get('os_service_type') or 'infra-optim', + interface=kwargs.get('os_endpoint_type') or 'publicURL', + region_name=kwargs.get('os_region_name')) + def test_get_client_no_auth_token(self): - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', fake_get_ksclient)) kwargs = { 'os_tenant_name': 'TENANT_NAME', 'os_username': 'USERNAME', @@ -48,17 +61,21 @@ class ClientTest(utils.BaseTestCase): 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': '', } - client = get_client('1', **kwargs) + self._test_get_client(**kwargs) - self.assertEqual('KSCLIENT_AUTH_TOKEN', client.http_client.auth_token) - self.assertEqual('http://localhost:6385/v1/f14b41234', - client.http_client.endpoint) - self.assertEqual(fake_get_ksclient().auth_ref, - client.http_client.auth_ref) + def test_get_client_service_and_endpoint_type_defaults(self): + kwargs = { + 'os_tenant_name': 'TENANT_NAME', + 'os_username': 'USERNAME', + 'os_password': 'PASSWORD', + 'os_auth_url': 'http://localhost:35357/v2.0', + 'os_auth_token': '', + 'os_service_type': '', + 'os_endpoint_type': '' + } + self._test_get_client(**kwargs) def test_get_client_with_region_no_auth_token(self): - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', fake_get_ksclient)) kwargs = { 'os_tenant_name': 'TENANT_NAME', 'os_username': 'USERNAME', @@ -67,76 +84,269 @@ class ClientTest(utils.BaseTestCase): 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': '', } - client = get_client('1', **kwargs) + self._test_get_client(**kwargs) - self.assertEqual('KSCLIENT_AUTH_TOKEN', client.http_client.auth_token) - self.assertEqual('http://regionhost:6385/v1/f14b41234', - client.http_client.endpoint) - self.assertEqual(fake_get_ksclient().auth_ref, - client.http_client.auth_ref) - - def test_get_client_with_auth_token(self): - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', fake_get_ksclient)) - kwargs = { - 'os_tenant_name': 'TENANT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_auth_token': 'USER_AUTH_TOKEN', - } - client = get_client('1', **kwargs) - - self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token) - self.assertEqual('http://localhost:6385/v1/f14b41234', - client.http_client.endpoint) - self.assertEqual(fake_get_ksclient().auth_ref, - client.http_client.auth_ref) - - def test_get_client_with_region_name_auth_token(self): - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', fake_get_ksclient)) - kwargs = { - 'os_tenant_name': 'TENANT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_region_name': 'REGIONONE', - 'os_auth_token': 'USER_AUTH_TOKEN', - } - client = get_client('1', **kwargs) - - self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token) - self.assertEqual('http://regionhost:6385/v1/f14b41234', - client.http_client.endpoint) - self.assertEqual(fake_get_ksclient().auth_ref, - client.http_client.auth_ref) - - def test_get_client_no_url_and_no_token(self): - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', fake_get_ksclient)) + def test_get_client_no_url(self): kwargs = { 'os_tenant_name': 'TENANT_NAME', 'os_username': 'USERNAME', 'os_password': 'PASSWORD', 'os_auth_url': '', - 'os_auth_token': '', } - self.assertRaises(exc.AmbiguousAuthSystem, get_client, '1', **kwargs) + self.assertRaises( + exceptions.AmbiguousAuthSystem, watcherclient.get_client, + '1', **kwargs) # test the alias as well to ensure backwards compatibility - self.assertRaises(exc.AmbigiousAuthSystem, get_client, '1', **kwargs) + self.assertRaises( + exceptions.AmbigiousAuthSystem, watcherclient.get_client, + '1', **kwargs) - def test_ensure_auth_ref_propagated(self): - ksclient = fake_get_ksclient - self.useFixture(fixtures.MonkeyPatch( - 'watcherclient.client._get_ksclient', ksclient)) + def test_get_client_incorrect_auth_params(self): + kwargs = { + 'os_tenant_name': 'TENANT_NAME', + 'os_username': 'USERNAME', + 'os_auth_url': 'http://localhost:35357/v2.0', + } + self.assertRaises( + exceptions.AmbiguousAuthSystem, watcherclient.get_client, + '1', **kwargs) + + def test_get_client_with_api_version_latest(self): kwargs = { 'os_tenant_name': 'TENANT_NAME', 'os_username': 'USERNAME', 'os_password': 'PASSWORD', 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': '', + 'os_watcher_api_version': "latest", } - client = get_client('1', **kwargs) + self._test_get_client(**kwargs) - self.assertEqual(ksclient().auth_ref, client.http_client.auth_ref) + def test_get_client_with_api_version_numeric(self): + kwargs = { + 'os_tenant_name': 'TENANT_NAME', + 'os_username': 'USERNAME', + 'os_password': 'PASSWORD', + 'os_auth_url': 'http://localhost:35357/v2.0', + 'os_auth_token': '', + 'os_watcher_api_version': "1.4", + } + self._test_get_client(**kwargs) + + def test_get_client_with_auth_token(self): + kwargs = { + 'os_auth_url': 'http://localhost:35357/v2.0', + 'os_auth_token': 'USER_AUTH_TOKEN', + } + self._test_get_client(auth='token', **kwargs) + + def test_get_client_with_region_name_auth_token(self): + kwargs = { + 'os_auth_url': 'http://localhost:35357/v2.0', + 'os_region_name': 'REGIONONE', + 'os_auth_token': 'USER_AUTH_TOKEN', + } + self._test_get_client(auth='token', **kwargs) + + def test_get_client_only_session_passed(self): + session = mock.Mock() + session.get_endpoint.return_value = 'http://localhost:35357/v2.0' + kwargs = { + 'session': session, + } + watcherclient.get_client('1', **kwargs) + session.get_endpoint.assert_called_once_with( + service_type='infra-optim', + interface='publicURL', + region_name=None) + + def test_get_client_incorrect_session_passed(self): + session = mock.Mock() + session.get_endpoint.side_effect = Exception('boo') + kwargs = { + 'session': session, + } + self.assertRaises( + exceptions.AmbiguousAuthSystem, watcherclient.get_client, + '1', **kwargs) + + @mock.patch.object(kaloading.session, 'Session', autospec=True) + @mock.patch.object(kaloading, 'get_plugin_loader', autospec=True) + def _test_loader_arguments_passed_correctly( + self, mock_ks_loader, mock_ks_session, + passed_kwargs, expected_kwargs): + session = mock_ks_session.return_value.load_from_options.return_value + session.get_endpoint.return_value = 'http://localhost:9322/v1/f14b4123' + mock_ks_loader.return_value.load_from_options.return_value = 'auth' + + watcherclient.get_client('1', **passed_kwargs) + + mock_ks_loader.return_value.load_from_options.assert_called_once_with( + **expected_kwargs) + mock_ks_session.return_value.load_from_options.assert_called_once_with( + auth='auth', timeout=passed_kwargs.get('timeout'), + insecure=passed_kwargs.get('insecure'), + cert=passed_kwargs.get('cert'), + cacert=passed_kwargs.get('cacert'), key=passed_kwargs.get('key')) + session.get_endpoint.assert_called_once_with( + service_type=passed_kwargs.get('os_service_type') or 'infra-optim', + interface=passed_kwargs.get('os_endpoint_type') or 'publicURL', + region_name=passed_kwargs.get('os_region_name')) + + def test_loader_arguments_token(self): + passed_kwargs = { + 'os_auth_url': 'http://localhost:35357/v3', + 'os_region_name': 'REGIONONE', + 'os_auth_token': 'USER_AUTH_TOKEN', + } + expected_kwargs = { + 'auth_url': 'http://localhost:35357/v3', + 'project_id': None, + 'project_name': None, + 'user_domain_id': None, + 'user_domain_name': None, + 'project_domain_id': None, + 'project_domain_name': None, + 'token': 'USER_AUTH_TOKEN' + } + self._test_loader_arguments_passed_correctly( + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs) + + def test_loader_arguments_password_tenant_name(self): + passed_kwargs = { + 'os_auth_url': 'http://localhost:35357/v3', + 'os_region_name': 'REGIONONE', + 'os_tenant_name': 'TENANT', + 'os_username': 'user', + 'os_password': '1234', + 'os_project_domain_id': 'DEFAULT', + 'os_user_domain_id': 'DEFAULT' + } + expected_kwargs = { + 'auth_url': 'http://localhost:35357/v3', + 'project_id': None, + 'project_name': 'TENANT', + 'user_domain_id': 'DEFAULT', + 'user_domain_name': None, + 'project_domain_id': 'DEFAULT', + 'project_domain_name': None, + 'username': 'user', + 'password': '1234' + } + self._test_loader_arguments_passed_correctly( + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs) + + def test_loader_arguments_password_project_id(self): + passed_kwargs = { + 'os_auth_url': 'http://localhost:35357/v3', + 'os_region_name': 'REGIONONE', + 'os_project_id': '1000', + 'os_username': 'user', + 'os_password': '1234', + 'os_project_domain_name': 'domain1', + 'os_user_domain_name': 'domain1' + } + expected_kwargs = { + 'auth_url': 'http://localhost:35357/v3', + 'project_id': '1000', + 'project_name': None, + 'user_domain_id': None, + 'user_domain_name': 'domain1', + 'project_domain_id': None, + 'project_domain_name': 'domain1', + 'username': 'user', + 'password': '1234' + } + self._test_loader_arguments_passed_correctly( + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs) + + @mock.patch.object(watcherclient, 'Client') + @mock.patch.object(kaloading.session, 'Session', autospec=True) + def test_correct_arguments_passed_to_client_constructor_noauth_mode( + self, mock_ks_session, mock_client): + kwargs = { + 'watcher_url': 'http://watcher.example.org:9322/', + 'os_auth_token': 'USER_AUTH_TOKEN', + 'os_watcher_api_version': 'latest', + 'insecure': True, + 'max_retries': 10, + 'retry_interval': 10, + 'os_cacert': 'data' + } + watcherclient.get_client('1', **kwargs) + mock_client.assert_called_once_with( + '1', 'http://watcher.example.org:9322/', + **{ + 'os_watcher_api_version': 'latest', + 'max_retries': 10, + 'retry_interval': 10, + 'token': 'USER_AUTH_TOKEN', + 'insecure': True, + 'ca_file': 'data', + 'cert_file': None, + 'key_file': None, + 'timeout': None, + 'session': None + } + ) + self.assertFalse(mock_ks_session.called) + + @mock.patch.object(watcherclient, 'Client') + @mock.patch.object(kaloading.session, 'Session', autospec=True) + def test_correct_arguments_passed_to_client_constructor_session_created( + self, mock_ks_session, mock_client): + session = mock_ks_session.return_value.load_from_options.return_value + kwargs = { + 'os_auth_url': 'http://localhost:35357/v3', + 'os_region_name': 'REGIONONE', + 'os_project_id': '1000', + 'os_username': 'user', + 'os_password': '1234', + 'os_project_domain_name': 'domain1', + 'os_user_domain_name': 'domain1' + } + watcherclient.get_client('1', **kwargs) + mock_client.assert_called_once_with( + '1', session.get_endpoint.return_value, + **{ + 'os_watcher_api_version': None, + 'max_retries': None, + 'retry_interval': None, + 'session': session, + } + ) + + @mock.patch.object(watcherclient, 'Client') + @mock.patch.object(kaloading.session, 'Session', autospec=True) + def test_correct_arguments_passed_to_client_constructor_session_passed( + self, mock_ks_session, mock_client): + session = mock.Mock() + kwargs = { + 'session': session, + } + watcherclient.get_client('1', **kwargs) + mock_client.assert_called_once_with( + '1', session.get_endpoint.return_value, + **{ + 'os_watcher_api_version': None, + 'max_retries': None, + 'retry_interval': None, + 'session': session, + } + ) + self.assertFalse(mock_ks_session.called) + + def test_safe_header_with_auth_token(self): + (name, value) = ('X-Auth-Token', u'3b640e2e64d946ac8f55615aff221dc1') + expected_header = (u'X-Auth-Token', + '{SHA1}6de9fb3b0b89099030a54abfeb468e7b1b1f0f2b') + client = httpclient.HTTPClient('http://localhost/') + header_redact = client._process_header(name, value) + self.assertEqual(expected_header, header_redact) + + def test_safe_header_with_no_auth_token(self): + name, value = ('Accept', 'application/json') + header = ('Accept', 'application/json') + client = httpclient.HTTPClient('http://localhost/') + header_redact = client._process_header(name, value) + self.assertEqual(header, header_redact) diff --git a/watcherclient/tests/test_http.py b/watcherclient/tests/test_http.py deleted file mode 100644 index b07f268..0000000 --- a/watcherclient/tests/test_http.py +++ /dev/null @@ -1,283 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2012 OpenStack LLC. -# 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 json - -import six - -from watcherclient.common import http -from watcherclient import exceptions as exc -from watcherclient.tests import utils - - -HTTP_CLASS = six.moves.http_client.HTTPConnection -HTTPS_CLASS = http.VerifiedHTTPSConnection -DEFAULT_TIMEOUT = 600 - - -def _get_error_body(faultstring=None, debuginfo=None): - error_body = { - 'faultstring': faultstring, - 'debuginfo': debuginfo - } - raw_error_body = json.dumps(error_body) - body = {'error_message': raw_error_body} - raw_body = json.dumps(body) - return raw_body - - -class HttpClientTest(utils.BaseTestCase): - - def test_url_generation_trailing_slash_in_base(self): - client = http.HTTPClient('http://localhost/') - url = client._make_connection_url('/v1/resources') - self.assertEqual('/v1/resources', url) - - def test_url_generation_without_trailing_slash_in_base(self): - client = http.HTTPClient('http://localhost') - url = client._make_connection_url('/v1/resources') - self.assertEqual('/v1/resources', url) - - def test_url_generation_prefix_slash_in_path(self): - client = http.HTTPClient('http://localhost/') - url = client._make_connection_url('/v1/resources') - self.assertEqual('/v1/resources', url) - - def test_url_generation_without_prefix_slash_in_path(self): - client = http.HTTPClient('http://localhost') - url = client._make_connection_url('v1/resources') - self.assertEqual('/v1/resources', url) - - def test_server_exception_empty_body(self): - error_body = _get_error_body() - fake_resp = utils.FakeResponse({'content-type': 'application/json'}, - six.StringIO(error_body), - version=1, - status=500) - client = http.HTTPClient('http://localhost/') - client.get_connection = ( - lambda *a, **kw: utils.FakeConnection(fake_resp)) - - error = self.assertRaises(exc.InternalServerError, - client.json_request, - 'GET', '/v1/resources') - self.assertEqual('Internal Server Error (HTTP 500)', str(error)) - - def test_server_exception_msg_only(self): - error_msg = 'test error msg' - error_body = _get_error_body(error_msg) - fake_resp = utils.FakeResponse({'content-type': 'application/json'}, - six.StringIO(error_body), - version=1, - status=500) - client = http.HTTPClient('http://localhost/') - client.get_connection = ( - lambda *a, **kw: utils.FakeConnection(fake_resp)) - - error = self.assertRaises(exc.InternalServerError, - client.json_request, - 'GET', '/v1/resources') - self.assertEqual(error_msg + ' (HTTP 500)', str(error)) - - def test_server_exception_msg_and_traceback(self): - error_msg = 'another test error' - error_trace = ("\"Traceback (most recent call last):\\n\\n " - "File \\\"/usr/local/lib/python2.7/...") - error_body = _get_error_body(error_msg, error_trace) - fake_resp = utils.FakeResponse({'content-type': 'application/json'}, - six.StringIO(error_body), - version=1, - status=500) - client = http.HTTPClient('http://localhost/') - client.get_connection = ( - lambda *a, **kw: utils.FakeConnection(fake_resp)) - - error = self.assertRaises(exc.InternalServerError, - client.json_request, - 'GET', '/v1/resources') - - self.assertEqual( - '%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg, - 'trace': error_trace}, - "%(error)s\n%(details)s" % {'error': str(error), - 'details': str(error.details)}) - - def test_get_connection_params(self): - endpoint = 'http://watcher-host:6385' - expected = (HTTP_CLASS, - ('watcher-host', 6385, ''), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_trailing_slash(self): - endpoint = 'http://watcher-host:6385/' - expected = (HTTP_CLASS, - ('watcher-host', 6385, ''), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_ssl(self): - endpoint = 'https://watcher-host:6385' - expected = (HTTPS_CLASS, - ('watcher-host', 6385, ''), - { - 'timeout': DEFAULT_TIMEOUT, - 'ca_file': None, - 'cert_file': None, - 'key_file': None, - 'insecure': False, - }) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_ssl_params(self): - endpoint = 'https://watcher-host:6385' - ssl_args = { - 'ca_file': '/path/to/ca_file', - 'cert_file': '/path/to/cert_file', - 'key_file': '/path/to/key_file', - 'insecure': True, - } - - expected_kwargs = {'timeout': DEFAULT_TIMEOUT} - expected_kwargs.update(ssl_args) - expected = (HTTPS_CLASS, - ('watcher-host', 6385, ''), - expected_kwargs) - params = http.HTTPClient.get_connection_params(endpoint, **ssl_args) - self.assertEqual(expected, params) - - def test_get_connection_params_with_timeout(self): - endpoint = 'http://watcher-host:6385' - expected = (HTTP_CLASS, - ('watcher-host', 6385, ''), - {'timeout': 300.0}) - params = http.HTTPClient.get_connection_params(endpoint, timeout=300) - self.assertEqual(expected, params) - - def test_get_connection_params_with_version(self): - endpoint = 'http://watcher-host:6385/v1' - expected = (HTTP_CLASS, - ('watcher-host', 6385, ''), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_version_trailing_slash(self): - endpoint = 'http://watcher-host:6385/v1/' - expected = (HTTP_CLASS, - ('watcher-host', 6385, ''), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_subpath(self): - endpoint = 'http://watcher-host:6385/watcher' - expected = (HTTP_CLASS, - ('watcher-host', 6385, '/watcher'), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_subpath_trailing_slash(self): - endpoint = 'http://watcher-host:6385/watcher/' - expected = (HTTP_CLASS, - ('watcher-host', 6385, '/watcher'), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_subpath_version(self): - endpoint = 'http://watcher-host:6385/watcher/v1' - expected = (HTTP_CLASS, - ('watcher-host', 6385, '/watcher'), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_get_connection_params_with_subpath_version_trailing_slash(self): - endpoint = 'http://watcher-host:6385/watcher/v1/' - expected = (HTTP_CLASS, - ('watcher-host', 6385, '/watcher'), - {'timeout': DEFAULT_TIMEOUT}) - params = http.HTTPClient.get_connection_params(endpoint) - self.assertEqual(expected, params) - - def test_401_unauthorized_exception(self): - error_body = _get_error_body() - fake_resp = utils.FakeResponse({'content-type': 'text/plain'}, - six.StringIO(error_body), - version=1, - status=401) - client = http.HTTPClient('http://localhost/') - client.get_connection = ( - lambda *a, **kw: utils.FakeConnection(fake_resp)) - - self.assertRaises(exc.Unauthorized, client.json_request, - 'GET', '/v1/resources') - - -class SessionClientTest(utils.BaseTestCase): - - def test_server_exception_msg_and_traceback(self): - error_msg = 'another test error' - error_trace = ("\"Traceback (most recent call last):\\n\\n " - "File \\\"/usr/local/lib/python2.7/...") - error_body = _get_error_body(error_msg, error_trace) - - fake_session = utils.FakeSession({'Content-Type': 'application/json'}, - error_body, - 500) - - client = http.SessionClient(session=fake_session, - auth=None, - interface=None, - service_type='publicURL', - region_name='', - service_name=None) - - error = self.assertRaises(exc.InternalServerError, - client.json_request, - 'GET', '/v1/resources') - - self.assertEqual( - '%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg, - 'trace': error_trace}, - "%(error)s\n%(details)s" % {'error': str(error), - 'details': str(error.details)}) - - def test_server_exception_empty_body(self): - error_body = _get_error_body() - - fake_session = utils.FakeSession({'Content-Type': 'application/json'}, - error_body, - 500) - - client = http.SessionClient(session=fake_session, - auth=None, - interface=None, - service_type='publicURL', - region_name='', - service_name=None) - - error = self.assertRaises(exc.InternalServerError, - client.json_request, - 'GET', '/v1/resources') - - self.assertEqual('Internal Server Error (HTTP 500)', str(error)) diff --git a/watcherclient/tests/utils.py b/watcherclient/tests/utils.py index 2826361..10aa2bf 100644 --- a/watcherclient/tests/utils.py +++ b/watcherclient/tests/utils.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2012 OpenStack LLC. # All Rights Reserved. # @@ -16,18 +14,16 @@ # under the License. import copy -import datetime import os import fixtures +import mock from oslo_utils import strutils -from oslotest import base import six - -from watcherclient.common import http +import testtools -class BaseTestCase(base.BaseTestCase): +class BaseTestCase(testtools.TestCase): def setUp(self): super(BaseTestCase, self).setUp() @@ -55,7 +51,7 @@ class FakeAPI(object): def raw_request(self, *args, **kwargs): response = self._request(*args, **kwargs) - body_iter = http.ResponseBodyIterator(six.StringIO(response[1])) + body_iter = iter(six.StringIO(response[1])) return FakeResponse(response[0]), body_iter def json_request(self, *args, **kwargs): @@ -77,6 +73,9 @@ class FakeConnection(object): def getresponse(self): return self._response + def __repr__(self): + return ("FakeConnection(response=%s)" % (self._response)) + class FakeResponse(object): def __init__(self, headers, body=None, version=None, status=None, @@ -88,8 +87,9 @@ class FakeResponse(object): """ self.headers = headers self.body = body - self.version = version - self.status = status + self.raw = mock.Mock() + self.raw.version = version + self.status_code = status self.reason = reason def getheaders(self): @@ -101,44 +101,37 @@ class FakeResponse(object): def read(self, amt): return self.body.read(amt) - -class FakeServiceCatalog(object): - def url_for(self, endpoint_type, service_type, attr=None, - filter_value=None): - if attr == 'region' and filter_value: - return 'http://regionhost:6385/v1/f14b41234' - else: - return 'http://localhost:6385/v1/f14b41234' - - -class FakeKeystone(object): - service_catalog = FakeServiceCatalog() - timestamp = datetime.datetime.utcnow() + datetime.timedelta(days=5) - - def __init__(self, auth_token): - self.auth_token = auth_token - self.auth_ref = { - 'token': {'expires': FakeKeystone.timestamp.strftime( - '%Y-%m-%dT%H:%M:%S.%f'), - 'id': 'd1a541311782870742235'} - } + def __repr__(self): + return ("FakeResponse(%s, body=%s, version=%s, status=%s, reason=%s)" % + (self.headers, self.body, self.version, self.status, + self.reason)) class FakeSessionResponse(object): - def __init__(self, headers, content=None, status_code=None): + def __init__(self, headers, content=None, status_code=None, version=None): self.headers = headers self.content = content self.status_code = status_code + self.raw = mock.Mock() + self.raw.version = version + self.reason = '' + + def iter_content(self, chunk_size): + return iter(self.content) class FakeSession(object): - def __init__(self, headers, content=None, status_code=None): + def __init__(self, headers, content=None, status_code=None, version=None): self.headers = headers self.content = content self.status_code = status_code + self.version = version + self.verify = False + self.cert = ('test_cert', 'test_key') def request(self, url, method, **kwargs): - return FakeSessionResponse(self.headers, self.content, - self.status_code) + request = FakeSessionResponse( + self.headers, self.content, self.status_code, self.version) + return request diff --git a/watcherclient/tests/v1/base.py b/watcherclient/tests/v1/base.py index 61f6c3a..8fdb9e2 100644 --- a/watcherclient/tests/v1/base.py +++ b/watcherclient/tests/v1/base.py @@ -18,10 +18,10 @@ import json import shlex import mock +from osc_lib import utils as oscutils -from watcherclient import shell +from watcherclient.common import httpclient from watcherclient.tests import utils -from watcherclient.v1 import client class CommandTestCase(utils.BaseTestCase): @@ -29,17 +29,29 @@ class CommandTestCase(utils.BaseTestCase): def setUp(self): super(CommandTestCase, self).setUp() - self.p_build_http_client = mock.patch.object( - client.Client, 'build_http_client') - self.m_build_http_client = self.p_build_http_client.start() + self.fake_env = { + 'debug': False, + 'insecure': False, + 'no_auth': False, + 'os_auth_token': '', + 'os_auth_url': 'http://127.0.0.1:5000/v2.0', + 'os_endpoint_override': 'http://watcher-endpoint:9322', + 'os_username': 'test', + 'os_password': 'test', + 'timeout': 600, + 'os_watcher_api_version': '1'} + self.m_env = mock.Mock( + name='m_env', + side_effect=lambda x, *args, **kwargs: self.fake_env.get( + x.lower(), kwargs.get('default', ''))) + self.p_env = mock.patch.object(oscutils, 'env', self.m_env) + self.p_env.start() + self.addCleanup(self.p_env.stop) - self.m_watcher_client = mock.Mock(side_effect=client.Client) - self.p_create_client = mock.patch.object( - shell.WatcherShell, 'create_client', self.m_watcher_client) - self.p_create_client.start() - - self.addCleanup(self.p_build_http_client.stop) - self.addCleanup(self.p_create_client.stop) + self.p_construct_http_client = mock.patch.object( + httpclient, '_construct_http_client') + self.m_construct_http_client = self.p_construct_http_client.start() + self.addCleanup(self.p_construct_http_client.stop) def run_cmd(self, cmd, formatting='json'): if formatting: diff --git a/watcherclient/tests/v1/test_action.py b/watcherclient/tests/v1/test_action.py index 3b72438..a2fda61 100644 --- a/watcherclient/tests/v1/test_action.py +++ b/watcherclient/tests/v1/test_action.py @@ -127,7 +127,7 @@ fake_responses_pagination = { 'GET': ( {}, {"actions": [ACTION1], - "next": "http://127.0.0.1:6385/v1/actions/?limit=1"} + "next": "http://127.0.0.1:9322/v1/actions/?limit=1"} ), }, '/v1/actions/?limit=1': diff --git a/watcherclient/tests/v1/test_action_plan.py b/watcherclient/tests/v1/test_action_plan.py index 554991e..92f8d3e 100644 --- a/watcherclient/tests/v1/test_action_plan.py +++ b/watcherclient/tests/v1/test_action_plan.py @@ -86,7 +86,7 @@ fake_responses_pagination = { 'GET': ( {}, {"action_plans": [ACTION_PLAN1], - "next": "http://127.0.0.1:6385/v1/action_plans/?limit=1"} + "next": "http://127.0.0.1:9322/v1/action_plans/?limit=1"} ), }, '/v1/action_plans/?limit=1': diff --git a/watcherclient/tests/v1/test_audit.py b/watcherclient/tests/v1/test_audit.py index 17a3ee5..96d9cdc 100644 --- a/watcherclient/tests/v1/test_audit.py +++ b/watcherclient/tests/v1/test_audit.py @@ -100,7 +100,7 @@ fake_responses_pagination = { 'GET': ( {}, {"audits": [AUDIT1], - "next": "http://127.0.0.1:6385/v1/audits/?limit=1"} + "next": "http://127.0.0.1:9322/v1/audits/?limit=1"} ), }, '/v1/audits/?limit=1': diff --git a/watcherclient/tests/v1/test_audit_template.py b/watcherclient/tests/v1/test_audit_template.py index f818fc8..3537ed6 100644 --- a/watcherclient/tests/v1/test_audit_template.py +++ b/watcherclient/tests/v1/test_audit_template.py @@ -157,7 +157,7 @@ fake_responses_pagination = { 'GET': ( {}, {"audit_templates": [AUDIT_TMPL1], - "next": "http://127.0.0.1:6385/v1/audit_templates/?limit=1"} + "next": "http://127.0.0.1:9322/v1/audit_templates/?limit=1"} ), }, '/v1/audit_templates/?limit=1': diff --git a/watcherclient/tests/v1/test_goal.py b/watcherclient/tests/v1/test_goal.py index 6be88f6..1b5c917 100644 --- a/watcherclient/tests/v1/test_goal.py +++ b/watcherclient/tests/v1/test_goal.py @@ -71,7 +71,7 @@ fake_responses_pagination = { 'GET': ( {}, {"goals": [GOAL1], - "next": "http://127.0.0.1:6385/v1/goals/?limit=1"} + "next": "http://127.0.0.1:9322/v1/goals/?limit=1"} ), }, '/v1/goals/?limit=1': diff --git a/watcherclient/tests/v1/test_metric_collector.py b/watcherclient/tests/v1/test_metric_collector.py deleted file mode 100644 index bc2910b..0000000 --- a/watcherclient/tests/v1/test_metric_collector.py +++ /dev/null @@ -1,321 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2013 Red Hat, 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 copy - -import testtools -from testtools import matchers - -from watcherclient.tests import utils -import watcherclient.v1.metric_collector - -METRIC_COLLECTOR1 = { - 'id': 1, - 'uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588', - 'category': 'cat1', - 'endpoint': 'http://metric_collector_1:6446', -} - -METRIC_COLLECTOR2 = { - 'id': 2, - 'uuid': '67653274-eb24-c7ba-70f6-a84e73d80843', - 'category': 'cat2', -} - -METRIC_COLLECTOR3 = { - 'id': 3, - 'uuid': 'f8e47706-efcf-49a4-a5c4-af604eb492f2', - 'category': 'cat2', - 'endpoint': 'http://metric_collector_3:6446', -} - -CREATE_METRIC_COLLECTOR = copy.deepcopy(METRIC_COLLECTOR1) -del CREATE_METRIC_COLLECTOR['id'] -del CREATE_METRIC_COLLECTOR['uuid'] - -UPDATED_METRIC_COLLECTOR1 = copy.deepcopy(METRIC_COLLECTOR1) -NEW_ENDPOINT = 'http://metric_collector_1:6447' -UPDATED_METRIC_COLLECTOR1['endpoint'] = NEW_ENDPOINT - -fake_responses = { - '/v1/metric-collectors': - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR1]}, - ), - 'POST': ( - {}, - CREATE_METRIC_COLLECTOR, - ), - }, - '/v1/metric-collectors/?category=%s' % METRIC_COLLECTOR1['category']: - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR1]}, - ), - }, - '/v1/metric-collectors/?category=%s' % METRIC_COLLECTOR2['category']: - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR3]}, - ), - }, - '/v1/metric-collectors/detail': - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR1]}, - ), - }, - '/v1/metric-collectors/%s' % METRIC_COLLECTOR1['uuid']: - { - 'GET': ( - {}, - METRIC_COLLECTOR1, - ), - 'DELETE': ( - {}, - None, - ), - 'PATCH': ( - {}, - UPDATED_METRIC_COLLECTOR1, - ), - }, - '/v1/metric-collectors/detail?category=%s' % METRIC_COLLECTOR1['category']: - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR1]}, - ), - }, - '/v1/metric-collectors/detail?category=%s' % METRIC_COLLECTOR2['category']: - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR3]}, - ), - }, -} - -fake_responses_pagination = { - '/v1/metric-collectors': - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR1], - "next": "http://127.0.0.1:6385/v1/metric-collectors/?limit=1"} - ), - }, - '/v1/metric-collectors/?limit=1': - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR2]} - ), - }, -} - -fake_responses_sorting = { - '/v1/metric-collectors/?sort_key=updated_at': - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR1]} - ), - }, - '/v1/metric-collectors/?sort_dir=desc': - { - 'GET': ( - {}, - {"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR1]} - ), - }, -} - - -class MetricCollectorManagerTest(testtools.TestCase): - - def setUp(self): - super(MetricCollectorManagerTest, self).setUp() - self.api = utils.FakeAPI(fake_responses) - self.mgr = watcherclient.v1.metric_collector \ - .MetricCollectorManager(self.api) - - def test_metric_collectors_list(self): - metric_collectors = self.mgr.list() - expect = [ - ('GET', '/v1/metric-collectors', {}, None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(1, len(metric_collectors)) - - def test_metric_collectors_list_by_category(self): - metric_collectors = self.mgr.list( - category=METRIC_COLLECTOR1['category'] - ) - expect = [ - ('GET', - '/v1/metric-collectors/?category=%s' % - METRIC_COLLECTOR1['category'], - {}, - None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(1, len(metric_collectors)) - - def test_metric_collectors_list_by_category_bis(self): - metric_collectors = self.mgr.list( - category=METRIC_COLLECTOR2['category'] - ) - expect = [ - ('GET', - '/v1/metric-collectors/?category=%s' % - METRIC_COLLECTOR2['category'], - {}, - None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(2, len(metric_collectors)) - - def test_metric_collectors_list_detail(self): - metric_collectors = self.mgr.list(detail=True) - expect = [ - ('GET', '/v1/metric-collectors/detail', {}, None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(1, len(metric_collectors)) - - def test_metric_collectors_list_by_category_detail(self): - metric_collectors = self.mgr.list( - category=METRIC_COLLECTOR1['category'], - detail=True) - expect = [ - ('GET', - '/v1/metric-collectors/detail?category=%s' % - METRIC_COLLECTOR1['category'], - {}, - None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(1, len(metric_collectors)) - - def test_metric_collectors_list_by_category_detail_bis(self): - metric_collectors = self.mgr.list( - category=METRIC_COLLECTOR2['category'], - detail=True) - expect = [ - ('GET', - '/v1/metric-collectors/detail?category=%s' % - METRIC_COLLECTOR2['category'], - {}, - None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(2, len(metric_collectors)) - - def test_metric_collectors_list_limit(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = watcherclient.v1.metric_collector \ - .MetricCollectorManager(self.api) - metric_collectors = self.mgr.list(limit=1) - expect = [ - ('GET', '/v1/metric-collectors/?limit=1', {}, None), - ] - self.assertEqual(expect, self.api.calls) - self.assertThat(metric_collectors, matchers.HasLength(1)) - - def test_metric_collectors_list_pagination_no_limit(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = watcherclient.v1.metric_collector \ - .MetricCollectorManager(self.api) - metric_collectors = self.mgr.list(limit=0) - expect = [ - ('GET', '/v1/metric-collectors', {}, None), - ('GET', '/v1/metric-collectors/?limit=1', {}, None) - ] - self.assertEqual(expect, self.api.calls) - self.assertThat(metric_collectors, matchers.HasLength(2)) - - def test_metric_collectors_list_sort_key(self): - self.api = utils.FakeAPI(fake_responses_sorting) - self.mgr = watcherclient.v1.metric_collector \ - .MetricCollectorManager(self.api) - metric_collectors = self.mgr.list(sort_key='updated_at') - expect = [ - ('GET', '/v1/metric-collectors/?sort_key=updated_at', {}, None) - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(2, len(metric_collectors)) - - def test_metric_collectors_list_sort_dir(self): - self.api = utils.FakeAPI(fake_responses_sorting) - self.mgr = watcherclient.v1.metric_collector \ - .MetricCollectorManager(self.api) - metric_collectors = self.mgr.list(sort_dir='desc') - expect = [ - ('GET', '/v1/metric-collectors/?sort_dir=desc', {}, None) - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(2, len(metric_collectors)) - - def test_metric_collectors_show(self): - metric_collector = self.mgr.get(METRIC_COLLECTOR1['uuid']) - expect = [ - ('GET', '/v1/metric-collectors/%s' % - METRIC_COLLECTOR1['uuid'], {}, None), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(METRIC_COLLECTOR1['uuid'], metric_collector.uuid) - self.assertEqual(METRIC_COLLECTOR1['category'], - metric_collector.category) - self.assertEqual(METRIC_COLLECTOR1['endpoint'], - metric_collector.endpoint) - - def test_create(self): - metric_collector = self.mgr.create(**CREATE_METRIC_COLLECTOR) - expect = [ - ('POST', '/v1/metric-collectors', {}, CREATE_METRIC_COLLECTOR), - ] - self.assertEqual(expect, self.api.calls) - self.assertTrue(metric_collector) - - def test_delete(self): - metric_collector = self.mgr.delete( - metric_collector_id=METRIC_COLLECTOR1['uuid']) - expect = [ - ('DELETE', '/v1/metric-collectors/%s' % - METRIC_COLLECTOR1['uuid'], {}, None), - ] - self.assertEqual(expect, self.api.calls) - self.assertIsNone(metric_collector) - - def test_update(self): - patch = {'op': 'replace', - 'value': NEW_ENDPOINT, - 'path': '/endpoint'} - metric_collector = self.mgr.update( - metric_collector_id=METRIC_COLLECTOR1['uuid'], patch=patch) - expect = [ - ('PATCH', '/v1/metric-collectors/%s' % - METRIC_COLLECTOR1['uuid'], {}, patch), - ] - self.assertEqual(expect, self.api.calls) - self.assertEqual(NEW_ENDPOINT, metric_collector.endpoint) diff --git a/watcherclient/tests/v1/test_scoring_engine.py b/watcherclient/tests/v1/test_scoring_engine.py index f8de21a..c308328 100644 --- a/watcherclient/tests/v1/test_scoring_engine.py +++ b/watcherclient/tests/v1/test_scoring_engine.py @@ -70,7 +70,7 @@ fake_responses_pagination = { 'GET': ( {}, {"scoring_engines": [SE1], - "next": "http://127.0.0.1:6385/v1/scoring_engines/?limit=1"} + "next": "http://127.0.0.1:9322/v1/scoring_engines/?limit=1"} ), }, '/v1/scoring_engines/?limit=1': diff --git a/watcherclient/tests/v1/test_strategy.py b/watcherclient/tests/v1/test_strategy.py index 25f9f9d..0b13cfa 100644 --- a/watcherclient/tests/v1/test_strategy.py +++ b/watcherclient/tests/v1/test_strategy.py @@ -73,7 +73,7 @@ fake_responses_pagination = { 'GET': ( {}, {"strategies": [STRATEGY1], - "next": "http://127.0.0.1:6385/v1/strategies/?limit=1"} + "next": "http://127.0.0.1:9322/v1/strategies/?limit=1"} ), }, '/v1/strategies/?limit=1': diff --git a/watcherclient/v1/client.py b/watcherclient/v1/client.py index 89e01fb..4facead 100644 --- a/watcherclient/v1/client.py +++ b/watcherclient/v1/client.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2012 OpenStack LLC. # All Rights Reserved. # @@ -15,7 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from watcherclient.common import http +from watcherclient._i18n import _ +from watcherclient.common import httpclient +from watcherclient import exceptions from watcherclient import v1 @@ -29,9 +29,25 @@ class Client(object): http requests. (optional) """ - def __init__(self, *args, **kwargs): + def __init__(self, endpoint=None, *args, **kwargs): """Initialize a new client for the Watcher v1 API.""" - self.http_client = self.build_http_client(*args, **kwargs) + if kwargs.get('os_watcher_api_version'): + kwargs['api_version_select_state'] = "user" + else: + if not endpoint: + raise exceptions.EndpointException( + _("Must provide 'endpoint' if os_watcher_api_version " + "isn't specified")) + + # If the user didn't specify a version, use a cached version if + # one has been stored + host, netport = httpclient.get_server(endpoint) + kwargs['api_version_select_state'] = "default" + kwargs['os_watcher_api_version'] = httpclient.DEFAULT_VER + + self.http_client = httpclient._construct_http_client( + endpoint, *args, **kwargs) + self.audit = v1.AuditManager(self.http_client) self.audit_template = v1.AuditTemplateManager(self.http_client) self.action = v1.ActionManager(self.http_client) @@ -39,7 +55,3 @@ class Client(object): self.goal = v1.GoalManager(self.http_client) self.scoring_engine = v1.ScoringEngineManager(self.http_client) self.strategy = v1.StrategyManager(self.http_client) - # self.metric_collector = v1.MetricCollectorManager(self.http_client) - - def build_http_client(self, *args, **kwargs): - return http._construct_http_client(*args, **kwargs) diff --git a/watcherclient/v1/metric_collector.py b/watcherclient/v1/metric_collector.py deleted file mode 100644 index ebf95ff..0000000 --- a/watcherclient/v1/metric_collector.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2013 Red Hat, 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 watcherclient.common import base -from watcherclient.common import utils -from watcherclient import exceptions as exc - -CREATION_ATTRIBUTES = ['endpoint', 'category'] - - -class MetricCollector(base.Resource): - def __repr__(self): - return "" % self._info - - -class MetricCollectorManager(base.Manager): - resource_class = MetricCollector - - @staticmethod - def _path(id=None): - return \ - '/v1/metric-collectors/%s' % id if id else '/v1/metric-collectors' - - def list(self, category=None, limit=None, sort_key=None, - sort_dir=None, detail=False): - """Retrieve a list of metric collector. - - :param category: Optional, Metric category, to get all metric - collectors mapped with this category. - :param limit: The maximum number of results to return per - request, if: - - 1) limit > 0, the maximum number of metric collectors to return. - 2) limit == 0, return the entire list of metriccollectors. - 3) limit param is NOT specified (None), the number of items - returned respect the maximum imposed by the Watcher API - (see Watcher's api.max_limit option). - - :param sort_key: Optional, field used for sorting. - - :param sort_dir: Optional, direction of sorting, either 'asc' (the - default) or 'desc'. - - :param detail: Optional, boolean whether to return detailed information - about metric collectors. - - :returns: A list of metric collectors. - - """ - if limit is not None: - limit = int(limit) - - filters = utils.common_filters(limit, sort_key, sort_dir) - if category is not None: - filters.append('category=%s' % category) - - path = '' - if detail: - path += 'detail' - if filters: - path += '?' + '&'.join(filters) - - if limit is None: - return self._list(self._path(path), "metric-collectors") - else: - return self._list_pagination(self._path(path), "metric-collectors", - limit=limit) - - def get(self, metric_collector_id): - try: - return self._list(self._path(metric_collector_id))[0] - except IndexError: - return None - - def create(self, **kwargs): - new = {} - for (key, value) in kwargs.items(): - if key in CREATION_ATTRIBUTES: - new[key] = value - else: - raise exc.InvalidAttribute() - return self._create(self._path(), new) - - def delete(self, metric_collector_id): - return self._delete(self._path(metric_collector_id)) - - def update(self, metric_collector_id, patch): - return self._update(self._path(metric_collector_id), patch)