switch to use non-legacy SessionClient

current client is just a rip of novaclient which uses legacy
methods. this brings it more inline with aodh/gnocchi clients
so it's easier to develop across all projects and not create a new
client using legacy stuff.

Change-Id: I27b3e4bd97dc1415803eddbecb8aed3849d40ab5
This commit is contained in:
gord chung 2017-05-16 19:28:22 +00:00
parent 5b51e41fcf
commit 3e6ac24b86
9 changed files with 241 additions and 1612 deletions

40
pankoclient/client.py Normal file
View File

@ -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

View File

@ -23,7 +23,7 @@ import copy
from requests import Response
import six
from pankoclient.common import exceptions
from pankoclient import exceptions
def getid(obj):

View File

@ -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)

View File

@ -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)

190
pankoclient/exceptions.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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())

View File

@ -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)