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: I27b3e4bd97dc1415803eddbecb8aed3849d40ab5changes/57/465157/5
parent
5b51e41fcf
commit
3e6ac24b86
@ -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
|
@ -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)
|
@ -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)
|
@ -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
|
@ -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)
|
@ -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',
|
||||