diff --git a/pankoclient/client.py b/pankoclient/client.py new file mode 100644 index 0000000..5e8614d --- /dev/null +++ b/pankoclient/client.py @@ -0,0 +1,40 @@ + +# 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 keystoneauth1 import adapter +from oslo_utils import importutils + +from pankoclient import exceptions + + +def Client(version, *args, **kwargs): + module = 'pankoclient.v%s.client' % version + module = importutils.import_module(module) + client_class = getattr(module, 'Client') + return client_class(*args, **kwargs) + + +class SessionClient(adapter.Adapter): + def request(self, url, method, **kwargs): + kwargs.setdefault('headers', kwargs.get('headers', {})) + # NOTE(sileht): The standard call raises errors from + # keystoneauth, where we need to raise the pankoclient errors. + raise_exc = kwargs.pop('raise_exc', True) + resp = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) + + if raise_exc and resp.status_code >= 400: + raise exceptions.from_response(resp, url, method) + return resp diff --git a/pankoclient/common/base.py b/pankoclient/common/base.py index fb575d3..6cc20f2 100644 --- a/pankoclient/common/base.py +++ b/pankoclient/common/base.py @@ -23,7 +23,7 @@ import copy from requests import Response import six -from pankoclient.common import exceptions +from pankoclient import exceptions def getid(obj): diff --git a/pankoclient/common/exceptions.py b/pankoclient/common/exceptions.py deleted file mode 100644 index d9fa28d..0000000 --- a/pankoclient/common/exceptions.py +++ /dev/null @@ -1,481 +0,0 @@ -# Copyright 2017 Huawei, 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 inspect -import sys - -from oslo_serialization import jsonutils -import six - -from pankoclient.common.i18n import _ - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises.""" - def __init__(self, message=None): - self.message = message - - def __str__(self): - return self.message or self.__class__.__doc__ - - -class ValidationError(ClientException): - """Error in validation on API client side.""" - pass - - -class UnsupportedVersion(ClientException): - """User is trying to use an unsupported version of the API.""" - pass - - -class CommandError(ClientException): - """Error in CLI tool.""" - pass - - -class AuthorizationFailure(ClientException): - """Cannot authorize API client.""" - pass - - -class ConnectionError(ClientException): - """Cannot connect to API service.""" - pass - - -class ConnectionRefused(ConnectionError): - """Connection refused while trying to connect to API service.""" - pass - - -class AuthPluginOptionsMissing(AuthorizationFailure): - """Auth plugin misses some options.""" - def __init__(self, opt_names): - super(AuthPluginOptionsMissing, self).__init__( - _("Authentication failed. Missing options: %s") % - ", ".join(opt_names)) - self.opt_names = opt_names - - -class AuthSystemNotFound(AuthorizationFailure): - """User has specified an AuthSystem that is not installed.""" - def __init__(self, auth_system): - super(AuthSystemNotFound, self).__init__( - _("AuthSystemNotFound: %r") % auth_system) - self.auth_system = auth_system - - -class NoUniqueMatch(ClientException): - """Multiple entities found instead of one.""" - pass - - -class EndpointException(ClientException): - """Something is rotten in Service Catalog.""" - pass - - -class EndpointNotFound(EndpointException): - """Could not find requested endpoint in Service Catalog.""" - pass - - -class AmbiguousEndpoints(EndpointException): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - super(AmbiguousEndpoints, self).__init__( - _("AmbiguousEndpoints: %r") % endpoints) - self.endpoints = endpoints - - -class HttpError(ClientException): - """The base exception class for all HTTP exceptions.""" - status_code = 0 - message = _("HTTP Error") - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, status_code=None): - self.status_code = status_code or self.status_code - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.status_code) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HTTPRedirection(HttpError): - """HTTP Redirection.""" - message = _("HTTP Redirection") - - -class HTTPClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = _("HTTP Client Error") - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = _("HTTP Server Error") - - -class MultipleChoices(HTTPRedirection): - """HTTP 300 - Multiple Choices. - - Indicates multiple options for the resource that the client may follow. - """ - - status_code = 300 - message = _("Multiple Choices") - - -class BadRequest(HTTPClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - status_code = 400 - message = _("Bad Request") - - -class Unauthorized(HTTPClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - status_code = 401 - message = _("Unauthorized") - - -class PaymentRequired(HTTPClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - status_code = 402 - message = _("Payment Required") - - -class Forbidden(HTTPClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - status_code = 403 - message = _("Forbidden") - - -class NotFound(HTTPClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - status_code = 404 - message = _("Not Found") - - -class MethodNotAllowed(HTTPClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - status_code = 405 - message = _("Method Not Allowed") - - -class NotAcceptable(HTTPClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - status_code = 406 - message = _("Not Acceptable") - - -class ProxyAuthenticationRequired(HTTPClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - status_code = 407 - message = _("Proxy Authentication Required") - - -class RequestTimeout(HTTPClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - status_code = 408 - message = _("Request Timeout") - - -class Conflict(HTTPClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - status_code = 409 - message = _("Conflict") - - -class Gone(HTTPClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - status_code = 410 - message = _("Gone") - - -class LengthRequired(HTTPClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - status_code = 411 - message = _("Length Required") - - -class PreconditionFailed(HTTPClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - status_code = 412 - message = _("Precondition Failed") - - -class RequestEntityTooLarge(HTTPClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - status_code = 413 - message = _("Request Entity Too Large") - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HTTPClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - status_code = 414 - message = _("Request-URI Too Long") - - -class UnsupportedMediaType(HTTPClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - status_code = 415 - message = _("Unsupported Media Type") - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - status_code = 416 - message = _("Requested Range Not Satisfiable") - - -class ExpectationFailed(HTTPClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - status_code = 417 - message = _("Expectation Failed") - - -class UnprocessableEntity(HTTPClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - status_code = 422 - message = _("Unprocessable Entity") - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - status_code = 500 - message = _("Internal Server Error") - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - status_code = 501 - message = _("Not Implemented") - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - status_code = 502 - message = _("Bad Gateway") - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - status_code = 503 - message = _("Service Unavailable") - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - status_code = 504 - message = _("Gateway Timeout") - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - status_code = 505 - message = _("HTTP Version Not Supported") - - -# _code_map contains all the classes that have status_code attribute. -_code_map = dict( - (getattr(obj, 'status_code', None), obj) - for name, obj in six.iteritems(vars(sys.modules[__name__])) - if inspect.isclass(obj) and getattr(obj, 'status_code', False) -) - - -def from_response(response, method, url): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - - # NOTE(liusheng): for pecan's response, the request_id is - # "Openstack-Request-Id" - req_id = (response.headers.get("x-openstack-request-id") or - response.headers.get("Openstack-Request-Id")) - kwargs = { - "status_code": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": req_id, - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if hasattr(body, 'keys'): - # NOTE(RuiChen): WebOb<1.6.0 will return a nested dict - # structure where the error keys to the message/details/code. - # WebOb>=1.6.0 returns just a response body as a single dict, - # not nested, so we have to handle both cases (since we can't - # trust what we're given with content_type: application/json - # either way. - if 'message' in body: - # WebOb>=1.6.0 case - error = body - else: - # WebOb<1.6.0 where we assume there is a single error - # message key to the body that has the message and details. - error = body.get(list(body)[0]) - # NOTE(liusheng): the response.json() may like this: - # {u'error_message': u'{"debuginfo": null, "faultcode": - # "Client", "faultstring": "error message"}'}, the - # "error_message" in the body is also a json string. - if isinstance(error, six.string_types): - error = jsonutils.loads(error) - - if hasattr(error, 'keys'): - kwargs['message'] = (error.get('message') or - error.get('faultstring')) - kwargs['details'] = (error.get('details') or - six.text_type(body)) - elif content_type.startswith("text/"): - kwargs["details"] = getattr(response, 'text', '') - - try: - cls = _code_map[response.status_code] - except KeyError: - if 500 <= response.status_code < 600: - cls = HttpServerError - elif 400 <= response.status_code < 500: - cls = HTTPClientError - else: - cls = HttpError - return cls(**kwargs) diff --git a/pankoclient/common/http.py b/pankoclient/common/http.py deleted file mode 100644 index 3783de1..0000000 --- a/pankoclient/common/http.py +++ /dev/null @@ -1,347 +0,0 @@ -# Copyright 2017 Huawei, 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 hashlib -import logging -import os -import socket - -from keystoneauth1 import adapter -from oslo_serialization import jsonutils -from oslo_utils import encodeutils -from oslo_utils import importutils -import requests -import six -from six.moves.urllib import parse - -from pankoclient.common import exceptions as exc -from pankoclient.common.i18n import _ -from pankoclient.common import utils - -LOG = logging.getLogger(__name__) - -USER_AGENT = 'python-pankoclient' -CHUNKSIZE = 1024 * 64 # 64kB -SENSITIVE_HEADERS = ('X-Auth-Token',) -osprofiler_web = importutils.try_import('osprofiler.web') - - -def get_system_ca_file(): - """Return path to system default CA file.""" - # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, - # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca - ca_path = ['/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem', - '/etc/ssl/cert.pem', - '/System/Library/OpenSSL/certs/cacert.pem', - requests.certs.where()] - for ca in ca_path: - LOG.debug("Looking for ca file %s", ca) - if os.path.exists(ca): - LOG.debug("Using ca file %s", ca) - return ca - LOG.warning("System ca file could not be found.") - - -class HTTPClient(object): - - def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint - self.auth_url = kwargs.get('auth_url') - self.auth_token = kwargs.get('token') - self.username = kwargs.get('username') - self.password = kwargs.get('password') - self.region_name = kwargs.get('region_name') - self.include_pass = kwargs.get('include_pass') - self.endpoint_url = endpoint - - self.cert_file = kwargs.get('cert_file') - self.key_file = kwargs.get('key_file') - self.timeout = kwargs.get('timeout') - - self.ssl_connection_params = { - 'ca_file': kwargs.get('ca_file'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), - 'insecure': kwargs.get('insecure'), - } - - self.verify_cert = None - if parse.urlparse(endpoint).scheme == "https": - if kwargs.get('insecure'): - self.verify_cert = False - else: - self.verify_cert = kwargs.get('ca_file', get_system_ca_file()) - - # FIXME(RuiChen): We need this for compatibility with the oslo - # apiclient we should move to inheriting this class from the oslo - # HTTPClient - self.last_request_id = None - - def safe_header(self, name, value): - if name in SENSITIVE_HEADERS: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return encodeutils.safe_decode(name), "{SHA1}%s" % d - else: - return (encodeutils.safe_decode(name), - encodeutils.safe_decode(value)) - - def log_curl_request(self, method, url, kwargs): - curl = ['curl -g -i -X %s' % method] - - for (key, value) in kwargs['headers'].items(): - header = '-H \'%s: %s\'' % self.safe_header(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.ssl_connection_params.get(key) - if value: - curl.append(fmt % value) - - if self.ssl_connection_params.get('insecure'): - curl.append('-k') - - if 'data' in kwargs: - curl.append('-d \'%s\'' % kwargs['data']) - - curl.append('%s%s' % (self.endpoint, url)) - LOG.debug(' '.join(curl)) - - @staticmethod - def log_http_response(resp): - 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 resp.content: - content = resp.content - if isinstance(content, six.binary_type): - content = content.decode() - dump.extend([content, '']) - LOG.debug('\n'.join(dump)) - - def _http_request(self, url, method, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around requests.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) - else: - kwargs['headers'].update(self.credentials_headers()) - if self.auth_url: - kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) - if self.region_name: - kwargs['headers'].setdefault('X-Region-Name', self.region_name) - if self.include_pass and 'X-Auth-Key' not in kwargs['headers']: - kwargs['headers'].update(self.credentials_headers()) - if osprofiler_web: - kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) - - self.log_curl_request(method, url, kwargs) - - if self.cert_file and self.key_file: - kwargs['cert'] = (self.cert_file, self.key_file) - - if self.verify_cert is not None: - kwargs['verify'] = self.verify_cert - - if self.timeout is not None: - kwargs['timeout'] = float(self.timeout) - - # Allow caller to specify not to follow redirects, in which case we - # just return the redirect response. Useful for using stacks:lookup. - redirect = kwargs.pop('redirect', True) - - # Since requests does not follow the RFC when doing redirection to sent - # back the same method on a redirect we are simply bypassing it. For - # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says - # that we should follow that URL with the same method as before, - # requests doesn't follow that and send a GET instead for the method. - # Hopefully this could be fixed as they say in a comment in a future - # point version i.e.: 3.x - # See issue: https://github.com/kennethreitz/requests/issues/1704 - allow_redirects = False - - # Use fully qualified URL from response header for redirects - if not parse.urlparse(url).netloc: - url = self.endpoint_url + url - - try: - resp = requests.request( - method, - url, - allow_redirects=allow_redirects, - **kwargs) - except socket.gaierror as e: - message = (_("Error finding address for %(url)s: %(e)s") % - {'url': self.endpoint_url + url, 'e': e}) - raise exc.EndpointNotFound(message=message) - except (socket.error, socket.timeout) as e: - endpoint = self.endpoint - message = (_("Error communicating with %(endpoint)s %(e)s") % - {'endpoint': endpoint, 'e': e}) - raise exc.ConnectionError(message=message) - - self.log_http_response(resp) - - if not ('X-Auth-Key' in kwargs['headers']) and ( - resp.status_code == 401 or - (resp.status_code == 500 and "(HTTP 401)" in resp.content)): - raise exc.AuthorizationFailure(_("Authentication failed: %s") - % resp.content) - elif 400 <= resp.status_code < 600: - raise exc.from_response(resp, method, url) - elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location, - # unless caller specified redirect=False - if redirect: - location = resp.headers.get('location') - location = self.strip_endpoint(location) - resp = self._http_request(location, method, **kwargs) - elif resp.status_code == 300: - raise exc.from_response(resp, method, url) - - return resp - - def strip_endpoint(self, location): - if location is None: - message = _("Location not returned with redirect") - raise exc.EndpointException(message=message) - if location.lower().startswith(self.endpoint): - return location[len(self.endpoint):] - else: - return location - - def credentials_headers(self): - creds = {} - # NOTE(RuiChen): When deferred_auth_method=password, Heat - # encrypts and stores username/password. For Keystone v3, the - # intent is to use trusts since SHARDY is working towards - # deferred_auth_method=trusts as the default. - if self.username: - creds['X-Auth-User'] = self.username - if self.password: - creds['X-Auth-Key'] = self.password - return creds - - def json_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - kwargs['headers'].setdefault('Accept', 'application/json') - - if 'data' in kwargs: - kwargs['data'] = jsonutils.dumps(kwargs['data']) - - resp = self._http_request(url, method, **kwargs) - body = utils.get_response_body(resp) - return resp, body - - def raw_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - resp = self._http_request(url, method, **kwargs) - body = utils.get_response_body(resp) - return resp, body - - def head(self, url, **kwargs): - return self.json_request("HEAD", url, **kwargs) - - def get(self, url, **kwargs): - return self.json_request("GET", url, **kwargs) - - def post(self, url, **kwargs): - return self.json_request("POST", url, **kwargs) - - def put(self, url, **kwargs): - return self.json_request("PUT", url, **kwargs) - - def delete(self, url, **kwargs): - return self.raw_request("DELETE", url, **kwargs) - - def patch(self, url, **kwargs): - return self.json_request("PATCH", url, **kwargs) - - -class SessionClient(adapter.LegacyJsonAdapter): - """HTTP client based on Keystone client session.""" - - def request(self, url, method, **kwargs): - redirect = kwargs.get('redirect') - kwargs.setdefault('user_agent', USER_AGENT) - - if 'data' in kwargs: - kwargs['json'] = kwargs.pop('data') - - resp, body = super(SessionClient, self).request( - url, method, - raise_exc=False, - **kwargs) - - if 400 <= resp.status_code < 600: - raise exc.from_response(resp, method, url) - elif resp.status_code in (301, 302, 305): - if redirect: - location = resp.headers.get('location') - path = self.strip_endpoint(location) - resp, body = self.request(path, method, **kwargs) - elif resp.status_code == 300: - raise exc.from_response(resp, method, url) - - return resp, body - - def credentials_headers(self): - return {} - - def strip_endpoint(self, location): - if location is None: - message = _("Location not returned with redirect") - raise exc.EndpointException(message=message) - if (self.endpoint_override is not None and - location.lower().startswith(self.endpoint_override.lower())): - return location[len(self.endpoint_override):] - else: - return location - - -def _construct_http_client(endpoint=None, username=None, password=None, - include_pass=None, endpoint_type=None, - auth_url=None, **kwargs): - session = kwargs.pop('session', None) - auth = kwargs.pop('auth', None) - - if session: - kwargs['endpoint_override'] = endpoint - return SessionClient(session, auth=auth, **kwargs) - else: - return HTTPClient(endpoint=endpoint, username=username, - password=password, include_pass=include_pass, - endpoint_type=endpoint_type, auth_url=auth_url, - **kwargs) diff --git a/pankoclient/exceptions.py b/pankoclient/exceptions.py new file mode 100644 index 0000000..317a94d --- /dev/null +++ b/pankoclient/exceptions.py @@ -0,0 +1,190 @@ +# +# 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. + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises.""" + message = 'Unknown Error' + http_status = 'N/A' + + def __init__(self, message=None, request_id=None, + url=None, method=None): + self.message = message or self.__class__.message + self.request_id = request_id + self.url = url + self.method = method + + # NOTE(jd) for backward compat + @property + def code(self): + return self.http_status + + def __str__(self): + formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) + if self.request_id: + formatted_string += " (Request-ID: %s)" % self.request_id + + return formatted_string + + +class RetryAfterException(ClientException): + """The base exception for ClientExceptions that use Retry-After header.""" + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RetryAfterException, self).__init__(*args, **kwargs) + + +class MutipleMeaningException(object): + """An mixin for exception that can be enhanced by reading the details""" + + +class CommandError(Exception): + pass + + +class BadRequest(ClientException): + """HTTP 400 - Bad request: you sent some malformed data.""" + http_status = 400 + message = "Bad request" + + +class Unauthorized(ClientException): + """HTTP 401 - Unauthorized: bad credentials.""" + http_status = 401 + message = "Unauthorized" + + +class Forbidden(ClientException): + """HTTP 403 - Forbidden: + + your credentials don't give you access to this resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(ClientException): + """HTTP 404 - Not found""" + http_status = 404 + message = "Not found" + + +class MethodNotAllowed(ClientException): + """HTTP 405 - Method Not Allowed""" + http_status = 405 + message = "Method Not Allowed" + + +class NotAcceptable(ClientException): + """HTTP 406 - Not Acceptable""" + http_status = 406 + message = "Not Acceptable" + + +class Conflict(ClientException): + """HTTP 409 - Conflict""" + http_status = 409 + message = "Conflict" + + +class OverLimit(RetryAfterException): + """HTTP 413 - Over limit: + + you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + +class RateLimit(RetryAfterException): + """HTTP 429 - Rate limit: + + you've sent too many requests for this time period. + """ + http_status = 429 + message = "Rate limit" + + +class NoUniqueMatch(Exception): + pass + + +class NotImplemented(ClientException): + """HTTP 501 - Not Implemented: + + the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, + MethodNotAllowed, NotAcceptable, Conflict, OverLimit, + RateLimit, NotImplemented] +_error_classes_enhanced = {} +_code_map = dict( + (c.http_status, (c, _error_classes_enhanced.get(c, []))) + for c in _error_classes) + + +def from_response(response, url, method=None): + """Return an instance of one of the ClientException on an requests response. + + Usage:: + resp, body = requests.request(...) + if resp.status_code != 200: + raise exception_from_response(resp) + """ + + if response.status_code: + cls, enhanced_classes = _code_map.get(response.status_code, + (ClientException, [])) + + req_id = response.headers.get("x-openstack-request-id") + content_type = response.headers.get("Content-Type", "").split(";")[0] + + kwargs = { + 'method': method, + 'url': url, + 'request_id': req_id, + } + + if "retry-after" in response.headers: + kwargs['retry_after'] = response.headers.get('retry-after') + + if content_type == "application/json": + try: + body = response.json() + except ValueError: + pass + else: + desc = body.get('error_message', {}).get('faultstring') + for enhanced_cls in enhanced_classes: + if enhanced_cls.match.match(desc): + cls = enhanced_cls + break + kwargs['message'] = desc + elif content_type.startswith("text/"): + kwargs['message'] = response.text + + if not kwargs.get('message'): + kwargs.pop('message', None) + + exception = cls(**kwargs) + if isinstance(exception, ClientException) and response.status_code: + exception.http_status = response.status_code + return exception diff --git a/pankoclient/tests/unit/common/test_base.py b/pankoclient/tests/unit/common/test_base.py index 43b28a0..6d05d4e 100644 --- a/pankoclient/tests/unit/common/test_base.py +++ b/pankoclient/tests/unit/common/test_base.py @@ -19,7 +19,7 @@ import mock import six from pankoclient.common import base -from pankoclient.common import exceptions +from pankoclient import exceptions from pankoclient.tests.unit import base as test_base from pankoclient.tests.unit import fakes diff --git a/pankoclient/tests/unit/common/test_exceptions.py b/pankoclient/tests/unit/common/test_exceptions.py deleted file mode 100644 index de28db5..0000000 --- a/pankoclient/tests/unit/common/test_exceptions.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2016 Huawei, 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 mock - -from pankoclient.common import exceptions as exc -from pankoclient.tests.unit import base - - -class TestHTTPExceptions(base.TestBase): - - def test_from_response(self): - mock_resp = mock.Mock() - mock_resp.status_code = 413 - mock_resp.json.return_value = { - 'entityTooLarge': { - 'code': 413, - 'message': 'Request Entity Too Large', - 'details': 'Error Details...', - } - } - mock_resp.headers = { - 'Content-Type': 'application/json', - 'x-openstack-request-id': mock.sentinel.fake_request_id, - 'retry-after': 10, - } - err = exc.from_response(mock_resp, 'POST', 'fake_url') - - self.assertIsInstance(err, exc.RequestEntityTooLarge) - self.assertEqual(413, err.status_code) - self.assertEqual('POST', err.method) - self.assertEqual('fake_url', err.url) - self.assertEqual('Request Entity Too Large (HTTP 413) (Request-ID: ' - 'sentinel.fake_request_id)', err.message) - self.assertEqual('Error Details...', err.details) - self.assertEqual(10, err.retry_after) - self.assertEqual(mock.sentinel.fake_request_id, err.request_id) - - def test_from_response_webob_new_format(self): - mock_resp = mock.Mock() - mock_resp.status_code = 413 - mock_resp.json.return_value = { - 'code': 413, - 'message': 'Request Entity Too Large', - 'details': 'Error Details...', - } - mock_resp.headers = { - 'Content-Type': 'application/json', - 'x-openstack-request-id': mock.sentinel.fake_request_id, - 'retry-after': 10, - } - err = exc.from_response(mock_resp, 'POST', 'fake_url') - - self.assertIsInstance(err, exc.RequestEntityTooLarge) - self.assertEqual(413, err.status_code) - self.assertEqual('POST', err.method) - self.assertEqual('fake_url', err.url) - self.assertEqual('Request Entity Too Large (HTTP 413) (Request-ID: ' - 'sentinel.fake_request_id)', err.message) - self.assertEqual('Error Details...', err.details) - self.assertEqual(10, err.retry_after) - self.assertEqual(mock.sentinel.fake_request_id, err.request_id) - - def test_from_response_pecan_response_format(self): - mock_resp = mock.Mock() - mock_resp.status_code = 400 - mock_resp.json.return_value = { - u'error_message': u'{"debuginfo": null, ' - u'"faultcode": "Client", ' - u'"faultstring": "Error Details..."}' - } - mock_resp.headers = { - 'Content-Type': 'application/json', - 'Openstack-Request-Id': 'fake_request_id', - 'Content-Length': '216', - 'Connection': 'keep-alive', - 'Date': 'Mon, 26 Dec 2016 06:59:04 GMT' - } - err = exc.from_response(mock_resp, 'POST', 'fake_url') - - self.assertEqual(400, err.status_code) - self.assertEqual('POST', err.method) - self.assertEqual('fake_url', err.url) - self.assertEqual( - 'Error Details... (HTTP 400) (Request-ID: fake_request_id)', - err.message) - self.assertEqual('fake_request_id', err.request_id) diff --git a/pankoclient/tests/unit/common/test_http.py b/pankoclient/tests/unit/common/test_http.py deleted file mode 100644 index 569303d..0000000 --- a/pankoclient/tests/unit/common/test_http.py +++ /dev/null @@ -1,679 +0,0 @@ -# Copyright 2016 Huawei, 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 socket - -from keystoneauth1 import adapter -import mock -from osc_lib.tests import fakes as osc_fakes -from oslo_serialization import jsonutils -import six - -from pankoclient.common import exceptions as exc -from pankoclient.common import http -from pankoclient.common import utils -from pankoclient.tests.unit import base -from pankoclient.tests.unit import fakes - - -@mock.patch('pankoclient.common.http.requests.request') -class TestHttpClient(base.TestBase): - - def setUp(self): - super(TestHttpClient, self).setUp() - - def test_http_raw_request(self, mock_request): - headers = {'User-Agent': 'python-pankoclient', - 'Content-Type': 'application/octet-stream'} - mock_request.return_value = fakes.FakeHTTPResponse(200, 'OK', {}, '') - client = http.HTTPClient('http://example.com:6688') - resp, body = client.raw_request('GET', '/prefix') - self.assertEqual(200, resp.status_code) - self.assertEqual('', ''.join([x for x in resp.content])) - mock_request.assert_called_once_with('GET', - 'http://example.com:6688/prefix', - allow_redirects=False, - headers=headers) - - def test_token_or_credentials(self, mock_request): - # Record a 200 - fake200 = fakes.FakeHTTPResponse(200, 'OK', {}, '') - mock_request.side_effect = [fake200, fake200, fake200] - - # Replay, create client, assert - client = http.HTTPClient('http://example.com:6688') - resp, body = client.raw_request('GET', '') - self.assertEqual(200, resp.status_code) - - client.username = osc_fakes.USERNAME - client.password = osc_fakes.PASSWORD - resp, body = client.raw_request('GET', '') - self.assertEqual(200, resp.status_code) - - client.auth_token = osc_fakes.AUTH_TOKEN - resp, body = client.raw_request('GET', '') - self.assertEqual(200, resp.status_code) - - # no token or credentials - mock_request.assert_has_calls([ - mock.call('GET', 'http://example.com:6688', - allow_redirects=False, - headers={'User-Agent': 'python-pankoclient', - 'Content-Type': 'application/octet-stream'}), - mock.call('GET', 'http://example.com:6688', - allow_redirects=False, - headers={'User-Agent': 'python-pankoclient', - 'X-Auth-Key': osc_fakes.PASSWORD, - 'X-Auth-User': osc_fakes.USERNAME, - 'Content-Type': 'application/octet-stream'}), - mock.call('GET', 'http://example.com:6688', - allow_redirects=False, - headers={'User-Agent': 'python-pankoclient', - 'X-Auth-Token': osc_fakes.AUTH_TOKEN, - 'Content-Type': 'application/octet-stream'}) - ]) - - def test_region_name(self, mock_request): - # Record a 200 - fake200 = fakes.FakeHTTPResponse(200, 'OK', {}, '') - mock_request.return_value = fake200 - - client = http.HTTPClient('http://example.com:6688') - client.region_name = osc_fakes.REGION_NAME - resp, body = client.raw_request('GET', '') - self.assertEqual(200, resp.status_code) - - mock_request.assert_called_once_with( - 'GET', 'http://example.com:6688', - allow_redirects=False, - headers={'X-Region-Name': osc_fakes.REGION_NAME, - 'User-Agent': 'python-pankoclient', - 'Content-Type': 'application/octet-stream'}) - - def test_http_json_request(self, mock_request): - # Record a 200 - mock_request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - - client = http.HTTPClient('http://example.com:6688') - resp, body = client.json_request('GET', '') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - - mock_request.assert_called_once_with( - 'GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_http_json_request_argument_passed_to_requests(self, mock_request): - """Check that we have sent the proper arguments to requests.""" - # Record a 200 - mock_request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - - client = http.HTTPClient('http://example.com:6688') - client.verify_cert = True - client.cert_file = 'RANDOM_CERT_FILE' - client.key_file = 'RANDOM_KEY_FILE' - client.auth_url = osc_fakes.AUTH_URL - resp, body = client.json_request('POST', '', data='text') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - - mock_request.assert_called_once_with( - 'POST', 'http://example.com:6688', - allow_redirects=False, - cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'), - verify=True, - data='"text"', - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-Auth-Url': osc_fakes.AUTH_URL, - 'User-Agent': 'python-pankoclient'}) - - def test_http_json_request_w_req_body(self, mock_request): - # Record a 200 - mock_request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - - client = http.HTTPClient('http://example.com:6688') - resp, body = client.json_request('POST', '', data='test-body') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - mock_request.assert_called_once_with( - 'POST', 'http://example.com:6688', - data='"test-body"', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_http_json_request_non_json_resp_cont_type(self, mock_request): - # Record a 200 - mock_request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'not/json'}, '{}') - - client = http.HTTPClient('http://example.com:6688') - resp, body = client.json_request('POST', '', data='test-data') - self.assertEqual(200, resp.status_code) - self.assertIsNone(body) - mock_request.assert_called_once_with( - 'POST', 'http://example.com:6688', data='"test-data"', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_http_json_request_invalid_json(self, mock_request): - # Record a 200 - mock_request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, 'invalid-json') - - client = http.HTTPClient('http://example.com:6688') - resp, body = client.json_request('GET', '') - self.assertEqual(200, resp.status_code) - self.assertEqual('invalid-json', body) - mock_request.assert_called_once_with( - 'GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_http_json_request_redirect_delete(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:6688/foo/bar'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'Content-Type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:6688/foo') - resp, body = client.json_request('DELETE', '') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('DELETE', 'http://example.com:6688/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}), - mock.call('DELETE', 'http://example.com:6688/foo/bar', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - ]) - - def test_http_json_request_redirect_post(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:6688/foo/bar'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'Content-Type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:6688/foo') - resp, body = client.json_request('POST', '') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('POST', 'http://example.com:6688/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}), - mock.call('POST', 'http://example.com:6688/foo/bar', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - ]) - - def test_http_json_request_redirect_put(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:6688/foo/bar'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'Content-Type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:6688/foo') - resp, body = client.json_request('PUT', '') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('PUT', 'http://example.com:6688/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}), - mock.call('PUT', 'http://example.com:6688/foo/bar', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - ]) - - def test_http_json_request_redirect_diff_location(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:6688/diff_lcation'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'Content-Type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:6688/foo') - resp, body = client.json_request('PUT', '') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('PUT', 'http://example.com:6688/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}), - mock.call('PUT', 'http://example.com:6688/diff_lcation', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - ]) - - def test_http_json_request_redirect_error_without_location(self, - mock_request): - mock_request.return_value = fakes.FakeHTTPResponse( - 302, 'Found', {}, '') - client = http.HTTPClient('http://example.com:6688/foo') - self.assertRaises(exc.EndpointException, - client.json_request, 'DELETE', '') - mock_request.assert_called_once_with( - 'DELETE', 'http://example.com:6688/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_http_json_request_redirect_get(self, mock_request): - # Record the 302 - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:6688'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'Content-Type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:6688') - resp, body = client.json_request('GET', '') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - - mock_request.assert_has_calls([ - mock.call('GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}), - mock.call('GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - ]) - - def test_http_404_json_request(self, mock_request): - mock_request.return_value = fakes.FakeHTTPResponse( - 404, 'Not Found', {'Content-Type': 'application/json'}, '') - - client = http.HTTPClient('http://example.com:6688') - e = self.assertRaises(exc.NotFound, client.json_request, 'GET', '') - # Assert that the raised exception can be converted to string - self.assertIsNotNone(str(e)) - # Record a 404 - mock_request.assert_called_once_with( - 'GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_http_300_json_request(self, mock_request): - mock_request.return_value = fakes.FakeHTTPResponse( - 300, 'OK', {'Content-Type': 'application/json'}, '') - client = http.HTTPClient('http://example.com:6688') - e = self.assertRaises( - exc.MultipleChoices, client.json_request, 'GET', '') - # Assert that the raised exception can be converted to string - self.assertIsNotNone(str(e)) - - # Record a 300 - mock_request.assert_called_once_with( - 'GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}) - - def test_fake_json_request(self, mock_request): - headers = {'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'} - mock_request.side_effect = [socket.gaierror] - - client = http.HTTPClient('fake://example.com:6688') - self.assertRaises(exc.EndpointNotFound, - client.json_request, "GET", "/") - mock_request.assert_called_once_with('GET', 'fake://example.com:6688/', - allow_redirects=False, - headers=headers) - - def test_http_request_socket_error(self, mock_request): - headers = {'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'} - mock_request.side_effect = [socket.error] - - client = http.HTTPClient('http://example.com:6688') - self.assertRaises(exc.ConnectionError, - client.json_request, "GET", "/") - mock_request.assert_called_once_with('GET', 'http://example.com:6688/', - allow_redirects=False, - headers=headers) - - def test_http_request_socket_timeout(self, mock_request): - headers = {'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'} - mock_request.side_effect = [socket.timeout] - - client = http.HTTPClient('http://example.com:6688') - self.assertRaises(exc.ConnectionError, - client.json_request, "GET", "/") - mock_request.assert_called_once_with('GET', 'http://example.com:6688/', - allow_redirects=False, - headers=headers) - - def test_http_request_specify_timeout(self, mock_request): - mock_request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - - client = http.HTTPClient('http://example.com:6688', timeout='123') - resp, body = client.json_request('GET', '') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - mock_request.assert_called_once_with( - 'GET', 'http://example.com:6688', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-pankoclient'}, - timeout=float(123)) - - def test_get_system_ca_file(self, mock_request): - chosen = '/etc/ssl/certs/ca-certificates.crt' - with mock.patch('os.path.exists') as mock_os: - mock_os.return_value = chosen - - ca = http.get_system_ca_file() - self.assertEqual(chosen, ca) - - mock_os.assert_called_once_with(chosen) - - def test_insecure_verify_cert_none(self, mock_request): - client = http.HTTPClient('https://foo', insecure=True) - self.assertFalse(client.verify_cert) - - def test_passed_cert_to_verify_cert(self, mock_request): - client = http.HTTPClient('https://foo', ca_file="NOWHERE") - self.assertEqual("NOWHERE", client.verify_cert) - - with mock.patch('pankoclient.common.http.get_system_ca_file') as gsf: - gsf.return_value = "SOMEWHERE" - client = http.HTTPClient('https://foo') - self.assertEqual("SOMEWHERE", client.verify_cert) - - def test_methods(self, mock_request): - fake = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - mock_request.return_value = fake - - client = http.HTTPClient('http://example.com:6688') - methods = [client.get, client.put, client.post, client.patch, - client.delete, client.head] - for method in methods: - resp, body = method('') - self.assertEqual(200, resp.status_code) - - -class TestSessionClient(base.TestBase): - - def setUp(self): - super(TestSessionClient, self).setUp() - self.request = mock.patch.object(adapter.LegacyJsonAdapter, - 'request').start() - - def test_session_simple_request(self): - resp = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/octet-stream'}, '{}') - self.request.return_value = (resp, {}) - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - resp, body = client.request(method='GET', url='') - self.assertEqual(200, resp.status_code) - self.assertEqual('{}', ''.join([x for x in resp.content])) - self.assertEqual({}, body) - - def test_session_json_request(self): - fake = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, - jsonutils.dumps({'some': 'body'})) - self.request.return_value = (fake, {'some': 'body'}) - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - - resp, body = client.request('', 'GET') - self.assertEqual(200, resp.status_code) - self.assertEqual({'some': 'body'}, resp.json()) - self.assertEqual({'some': 'body'}, body) - - def test_404_error_response(self): - fake = fakes.FakeHTTPResponse( - 404, 'Not Found', {'Content-Type': 'application/json'}, '') - self.request.return_value = (fake, '') - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - e = self.assertRaises(exc.NotFound, - client.request, '', 'GET') - # Assert that the raised exception can be converted to string - self.assertIsNotNone(six.text_type(e)) - - def test_redirect_302_location(self): - fake1 = fakes.FakeHTTPResponse( - 302, 'OK', {'location': 'http://no.where/ishere'}, '') - fake2 = fakes.FakeHTTPResponse(200, 'OK', - {'Content-Type': 'application/json'}, - jsonutils.dumps({'Mount': 'Fuji'})) - self.request.side_effect = [ - (fake1, None), (fake2, {'Mount': 'Fuji'})] - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY, - endpoint_override='http://no.where/') - resp, body = client.request('', 'GET', redirect=True) - - self.assertEqual(200, resp.status_code) - self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp)) - self.assertEqual({'Mount': 'Fuji'}, body) - - self.assertEqual(('', 'GET'), self.request.call_args_list[0][0]) - self.assertEqual(('ishere', 'GET'), self.request.call_args_list[1][0]) - for call in self.request.call_args_list: - self.assertEqual({'user_agent': 'python-pankoclient', - 'raise_exc': False, - 'redirect': True}, call[1]) - - def test_302_location_not_override(self): - fake1 = fakes.FakeHTTPResponse( - 302, 'OK', {'location': 'http://no.where/ishere'}, '') - fake2 = fakes.FakeHTTPResponse(200, 'OK', - {'Content-Type': 'application/json'}, - jsonutils.dumps({'Mount': 'Fuji'})) - self.request.side_effect = [ - (fake1, None), (fake2, {'Mount': 'Fuji'})] - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY, - endpoint_override='http://endpoint/') - resp, body = client.request('', 'GET', redirect=True) - - self.assertEqual(200, resp.status_code) - self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp)) - self.assertEqual({'Mount': 'Fuji'}, body) - - self.assertEqual(('', 'GET'), self.request.call_args_list[0][0]) - self.assertEqual(('http://no.where/ishere', - 'GET'), self.request.call_args_list[1][0]) - for call in self.request.call_args_list: - self.assertEqual({'user_agent': 'python-pankoclient', - 'raise_exc': False, - 'redirect': True}, call[1]) - - def test_redirect_302_no_location(self): - fake = fakes.FakeHTTPResponse( - 302, 'OK', {}, '') - self.request.side_effect = [(fake, '')] - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - e = self.assertRaises(exc.EndpointException, - client.request, '', 'GET', redirect=True) - self.assertEqual("Location not returned with redirect", - six.text_type(e)) - - def test_no_redirect_302_no_location(self): - fake = fakes.FakeHTTPResponse(302, 'OK', - {'location': 'http://no.where/ishere'}, - '') - self.request.side_effect = [(fake, '')] - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - resp, body = client.request('', 'GET') - - self.assertEqual(fake, resp) - - def test_300_error_response(self): - fake = fakes.FakeHTTPResponse( - 300, 'FAIL', {'Content-Type': 'application/octet-stream'}, '') - self.request.return_value = (fake, '') - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - e = self.assertRaises(exc.MultipleChoices, - client.request, '', 'GET') - # Assert that the raised exception can be converted to string - self.assertIsNotNone(six.text_type(e)) - - def test_506_error_response(self): - # for 506 we don't have specific exception type - fake = fakes.FakeHTTPResponse( - 506, 'FAIL', {'Content-Type': 'application/octet-stream'}, '') - self.request.return_value = (fake, '') - - client = http.SessionClient(session=mock.ANY, - auth=mock.ANY) - e = self.assertRaises(exc.HttpServerError, - client.request, '', 'GET') - - self.assertEqual(506, e.status_code) - - def test_kwargs(self): - fake = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - kwargs = dict(endpoint_override='http://no.where/', - data='some_data') - - client = http.SessionClient(mock.ANY) - - self.request.return_value = (fake, {}) - - resp, body = client.request('', 'GET', **kwargs) - - self.assertEqual({'endpoint_override': 'http://no.where/', - 'json': 'some_data', - 'user_agent': 'python-pankoclient', - 'raise_exc': False}, self.request.call_args[1]) - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - self.assertEqual({}, utils.get_response_body(resp)) - - @mock.patch.object(jsonutils, 'dumps') - def test_kwargs_with_files(self, mock_dumps): - fake = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - mock_dumps.return_value = "{'files': test}}" - data = six.BytesIO(b'test') - kwargs = {'endpoint_override': 'http://no.where/', - 'data': {'files': data}} - client = http.SessionClient(mock.ANY) - - self.request.return_value = (fake, {}) - - resp, body = client.request('', 'GET', **kwargs) - - self.assertEqual({'endpoint_override': 'http://no.where/', - 'json': {'files': data}, - 'user_agent': 'python-pankoclient', - 'raise_exc': False}, self.request.call_args[1]) - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - self.assertEqual({}, utils.get_response_body(resp)) - - def test_methods(self): - fake = fakes.FakeHTTPResponse( - 200, 'OK', {'Content-Type': 'application/json'}, '{}') - self.request.return_value = (fake, {}) - - client = http.SessionClient(mock.ANY) - methods = [client.get, client.put, client.post, client.patch, - client.delete, client.head] - for method in methods: - resp, body = method('') - self.assertEqual(200, resp.status_code) - - def test_credentials_headers(self): - client = http.SessionClient(mock.ANY) - self.assertEqual({}, client.credentials_headers()) diff --git a/pankoclient/v2/client.py b/pankoclient/v2/client.py index c0bccf6..e39b67f 100644 --- a/pankoclient/v2/client.py +++ b/pankoclient/v2/client.py @@ -13,17 +13,22 @@ # under the License. # -from pankoclient.common import http +from pankoclient import client from pankoclient.v2 import capabilities from pankoclient.v2 import events class Client(object): - """Client for the Panko v2 API.""" + """Client for the Panko v2 API - def __init__(self, *args, **kwargs): + :param string session: session + :type session: :py:class:`keystoneauth.adapter.Adapter` + """ + + def __init__(self, session=None, service_type='event', **kwargs): """Initialize a new client for the Panko v2 API.""" - self.http_client = http._construct_http_client(*args, **kwargs) + self.http_client = client.SessionClient( + session, service_type=service_type, **kwargs) self.capabilities = capabilities.CapabilitiesManager(self.http_client) self.event = events.EventManager(self.http_client) self.event_type = events.EventTypeManager(self.http_client)