Add new common/http
Change-Id: I35ac71af1f648537231b01cca9be455bddd3cc80
This commit is contained in:
parent
e76f8b02bc
commit
b28abd04d2
@ -13,48 +13,24 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import warnings
|
||||
|
||||
from glareclient.common import utils
|
||||
|
||||
|
||||
def Client(version=None, endpoint=None, session=None, *args, **kwargs):
|
||||
def Client(version='1', endpoint=None, session=None, *args, **kwargs):
|
||||
"""Client for the Glare Artifact Repository.
|
||||
|
||||
Generic client for the Glare Artifact Repository. See version classes
|
||||
for specific details.
|
||||
|
||||
:param string version: The version of API to use.
|
||||
:param session: A keystoneauth1 session that should be used for transport.
|
||||
:type session: keystoneauth1.session.Session
|
||||
"""
|
||||
# FIXME(jamielennox): Add a deprecation warning if no session is passed.
|
||||
# Leaving it as an option until we can ensure nothing break when we switch.
|
||||
if session:
|
||||
if endpoint:
|
||||
kwargs.setdefault('endpoint_override', endpoint)
|
||||
|
||||
if not version:
|
||||
__, version = utils.strip_version(endpoint)
|
||||
if endpoint is not None:
|
||||
kwargs.setdefault('endpoint_override', endpoint)
|
||||
|
||||
if not version:
|
||||
msg = ("You must provide a client version when using session")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
else:
|
||||
if version is not None:
|
||||
warnings.warn(("`version` keyword is being deprecated. Please pass"
|
||||
" the version as part of the URL. "
|
||||
"http://$HOST:$PORT/v$VERSION_NUMBER"),
|
||||
DeprecationWarning)
|
||||
|
||||
endpoint, url_version = utils.strip_version(endpoint)
|
||||
version = version or url_version
|
||||
|
||||
if not version:
|
||||
msg = ("Please provide either the version or an url with the form "
|
||||
"http://$HOST:$PORT/v$VERSION_NUMBER")
|
||||
raise RuntimeError(msg)
|
||||
if version is None:
|
||||
raise RuntimeError("You must provide a client version")
|
||||
|
||||
module = utils.import_versioned_module(int(version), 'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2012 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -14,102 +14,79 @@
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import hashlib
|
||||
import os
|
||||
import socket
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from keystoneauth1 import exceptions as ksa_exc
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import netutils
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
import requests
|
||||
import six
|
||||
import warnings
|
||||
from six.moves import urllib
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
|
||||
from glareclient.common import utils
|
||||
from glareclient import exc
|
||||
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
from glareclient._i18n import _
|
||||
from glareclient.common import exceptions as exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-glareclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
|
||||
|
||||
def encode_headers(headers):
|
||||
"""Encodes headers.
|
||||
|
||||
Note: This should be used right before
|
||||
sending anything out.
|
||||
|
||||
:param headers: Headers to encode
|
||||
:returns: Dictionary with encoded headers'
|
||||
names and values
|
||||
"""
|
||||
return dict((encodeutils.safe_encode(h), encodeutils.safe_encode(v))
|
||||
for h, v in six.iteritems(headers) if v is not None)
|
||||
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 _BaseHTTPClient(object):
|
||||
def _chunk_body(body):
|
||||
chunk = body
|
||||
while chunk:
|
||||
chunk = body.read(CHUNKSIZE)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
@staticmethod
|
||||
def _chunk_body(body):
|
||||
chunk = body
|
||||
while chunk:
|
||||
chunk = body.read(CHUNKSIZE)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
def _set_common_request_kwargs(self, headers, kwargs):
|
||||
"""Handle the common parameters used to send the request."""
|
||||
def _set_request_params(kwargs_params):
|
||||
data = kwargs_params.pop('data', None)
|
||||
params = copy.deepcopy(kwargs_params)
|
||||
headers = params.get('headers', {})
|
||||
content_type = headers.get('Content-Type', 'application/json')
|
||||
|
||||
# Default Content-Type is octet-stream
|
||||
content_type = headers.get('Content-Type', 'application/json')
|
||||
data = kwargs.pop("data", None)
|
||||
if data is not None and not isinstance(data, six.string_types):
|
||||
try:
|
||||
data = json.dumps(data)
|
||||
except TypeError:
|
||||
data = self._chunk_body(data)
|
||||
content_type = 'application/octet-stream'
|
||||
if data is not None and not isinstance(data, six.string_types):
|
||||
if content_type.startswith('application/json'):
|
||||
data = jsonutils.dumps(data)
|
||||
if content_type == 'application/octet-stream':
|
||||
data = _chunk_body(data)
|
||||
|
||||
headers['Content-Type'] = content_type
|
||||
kwargs['stream'] = content_type == 'application/octet-stream'
|
||||
return data
|
||||
params['data'] = data
|
||||
headers.update({'Content-Type': content_type})
|
||||
params['headers'] = headers
|
||||
params['stream'] = content_type == 'application/octet-stream'
|
||||
|
||||
def _handle_response(self, resp):
|
||||
# log request-id for each api cal
|
||||
request_id = resp.headers.get('x-openstack-request-id')
|
||||
if request_id:
|
||||
LOG.debug('%(method)s call to glare-api for '
|
||||
'%(url)s used request id '
|
||||
'%(response_request_id)s',
|
||||
{'method': resp.request.method,
|
||||
'url': resp.url,
|
||||
'response_request_id': request_id})
|
||||
return params
|
||||
|
||||
if not resp.ok:
|
||||
LOG.debug("Request returned failure status %s.", resp.status_code)
|
||||
raise exc.from_response(resp, resp.content)
|
||||
elif (resp.status_code == requests.codes.MULTIPLE_CHOICES and
|
||||
resp.request.path_url != '/versions'):
|
||||
# NOTE(flaper87): Eventually, we'll remove the check on `versions`
|
||||
# which is a bug (1491350) on the server.
|
||||
raise exc.from_response(resp)
|
||||
|
||||
def _handle_response(resp):
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
|
||||
if not content_type:
|
||||
body_iter = six.StringIO(resp.text)
|
||||
try:
|
||||
body_iter = json.loads(''.join([c for c in body_iter]))
|
||||
body_iter = jsonutils.loads(''.join([c for c in body_iter]))
|
||||
except ValueError:
|
||||
body_iter = None
|
||||
elif content_type.startswith('application/json'):
|
||||
@ -122,172 +99,6 @@ class _BaseHTTPClient(object):
|
||||
return resp, body_iter
|
||||
|
||||
|
||||
class HTTPClient(_BaseHTTPClient):
|
||||
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
self.identity_headers = kwargs.get('identity_headers')
|
||||
self.auth_token = kwargs.get('token')
|
||||
self.language_header = kwargs.get('language_header')
|
||||
self.last_request_id = None
|
||||
if self.identity_headers:
|
||||
if self.identity_headers.get('X-Auth-Token'):
|
||||
self.auth_token = self.identity_headers.get('X-Auth-Token')
|
||||
del self.identity_headers['X-Auth-Token']
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers["User-Agent"] = USER_AGENT
|
||||
|
||||
if self.language_header:
|
||||
self.session.headers["Accept-Language"] = self.language_header
|
||||
|
||||
self.timeout = float(kwargs.get('timeout', 600))
|
||||
|
||||
if self.endpoint.startswith("https"):
|
||||
compression = kwargs.get('ssl_compression', True)
|
||||
|
||||
if compression is False:
|
||||
# Note: This is not seen by default. (python must be
|
||||
# run with -Wd)
|
||||
warnings.warn('The "ssl_compression" argument has been '
|
||||
'deprecated.', DeprecationWarning)
|
||||
|
||||
if kwargs.get('insecure', False) is True:
|
||||
self.session.verify = False
|
||||
else:
|
||||
if kwargs.get('cacert', None) is not '':
|
||||
self.session.verify = kwargs.get('cacert', True)
|
||||
|
||||
self.session.cert = (kwargs.get('cert_file'),
|
||||
kwargs.get('key_file'))
|
||||
|
||||
@staticmethod
|
||||
def parse_endpoint(endpoint):
|
||||
return netutils.urlsplit(endpoint)
|
||||
|
||||
def log_curl_request(self, method, url, headers, data, kwargs):
|
||||
curl = ['curl -g -i -X %s' % method]
|
||||
|
||||
headers = copy.deepcopy(headers)
|
||||
headers.update(self.session.headers)
|
||||
|
||||
for (key, value) in six.iteritems(headers):
|
||||
header = '-H \'%s: %s\'' % utils.safe_header(key, value)
|
||||
curl.append(header)
|
||||
|
||||
if not self.session.verify:
|
||||
curl.append('-k')
|
||||
else:
|
||||
if isinstance(self.session.verify, six.string_types):
|
||||
curl.append(' --cacert %s' % self.session.verify)
|
||||
|
||||
if self.session.cert:
|
||||
curl.append(' --cert %s --key %s' % self.session.cert)
|
||||
|
||||
if data and isinstance(data, six.string_types):
|
||||
curl.append('-d \'%s\'' % data)
|
||||
|
||||
curl.append(url)
|
||||
|
||||
msg = ' '.join([encodeutils.safe_decode(item, errors='ignore')
|
||||
for item in curl])
|
||||
LOG.debug(msg)
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp):
|
||||
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
headers = resp.headers.items()
|
||||
dump.extend(['%s: %s' % utils.safe_header(k, v) for k, v in headers])
|
||||
dump.append('')
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
|
||||
if content_type != 'application/octet-stream':
|
||||
dump.extend([resp.text, ''])
|
||||
LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore')
|
||||
for x in dump]))
|
||||
|
||||
def _request(self, method, url, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
|
||||
as setting headers and error handling.
|
||||
"""
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
headers = copy.deepcopy(kwargs.pop('headers', {}))
|
||||
|
||||
if self.identity_headers:
|
||||
for k, v in six.iteritems(self.identity_headers):
|
||||
headers.setdefault(k, v)
|
||||
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
|
||||
# add identity header to the request
|
||||
if not headers.get('X-Auth-Token'):
|
||||
headers['X-Auth-Token'] = self.auth_token
|
||||
|
||||
if osprofiler_web:
|
||||
headers.update(osprofiler_web.get_trace_id_headers())
|
||||
|
||||
# Note(flaper87): Before letting headers / url fly,
|
||||
# they should be encoded otherwise httplib will
|
||||
# complain.
|
||||
headers = encode_headers(headers)
|
||||
|
||||
if self.endpoint.endswith("/") or url.startswith("/"):
|
||||
conn_url = "%s%s" % (self.endpoint, url)
|
||||
else:
|
||||
conn_url = "%s/%s" % (self.endpoint, url)
|
||||
self.log_curl_request(method, conn_url, headers, data, kwargs)
|
||||
|
||||
try:
|
||||
resp = self.session.request(method,
|
||||
conn_url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
**kwargs)
|
||||
except requests.exceptions.Timeout as e:
|
||||
message = ("Error communicating with %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
message = ("Error finding address for %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.CommunicationError(message=message)
|
||||
except socket.gaierror as e:
|
||||
message = "Error finding address for %s: %s" % (
|
||||
self.endpoint_hostname, e)
|
||||
raise exc.InvalidEndpoint(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.CommunicationError(message=message)
|
||||
|
||||
self.last_request_id = resp.headers.get('x-openstack-request-id')
|
||||
resp, body_iter = self._handle_response(resp)
|
||||
self.log_http_response(resp)
|
||||
return resp, body_iter
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self._request('HEAD', url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._request('GET', url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._request('POST', url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._request('PUT', url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self._request('PATCH', url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._request('DELETE', url, **kwargs)
|
||||
|
||||
|
||||
def _close_after_stream(response, chunk_size):
|
||||
"""Iterate over the content and ensure the response is closed after."""
|
||||
# Yield each chunk in the response body
|
||||
@ -299,45 +110,278 @@ def _close_after_stream(response, chunk_size):
|
||||
response.close()
|
||||
|
||||
|
||||
class SessionClient(adapter.Adapter, _BaseHTTPClient):
|
||||
class HTTPClient(object):
|
||||
|
||||
def __init__(self, session, **kwargs):
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
kwargs.setdefault('service_type', 'artifact')
|
||||
self.last_request_id = None
|
||||
super(SessionClient, self).__init__(session, **kwargs)
|
||||
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
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
headers = encode_headers(kwargs.pop('headers', {}))
|
||||
kwargs['raise_exc'] = False
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
self.cert_file = kwargs.get('cert_file')
|
||||
self.key_file = kwargs.get('key_file')
|
||||
self.timeout = kwargs.get('timeout')
|
||||
|
||||
self.ssl_connection_params = {
|
||||
'cacert': kwargs.get('cacert'),
|
||||
'cert_file': kwargs.get('cert_file'),
|
||||
'key_file': kwargs.get('key_file'),
|
||||
'insecure': kwargs.get('insecure'),
|
||||
}
|
||||
|
||||
self.verify_cert = None
|
||||
if urllib.parse.urlparse(endpoint).scheme == "https":
|
||||
if kwargs.get('insecure'):
|
||||
self.verify_cert = False
|
||||
else:
|
||||
self.verify_cert = kwargs.get('cacert', get_system_ca_file())
|
||||
|
||||
def _safe_header(self, name, value):
|
||||
if name in ['X-Auth-Token', 'X-Subject-Token']:
|
||||
# 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, url, method, kwargs):
|
||||
curl = ['curl -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'),
|
||||
('cacert', '--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):
|
||||
try:
|
||||
content = encodeutils.safe_decode(resp.content)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
else:
|
||||
dump.extend([content, ''])
|
||||
LOG.debug('\n'.join(dump))
|
||||
|
||||
def request(self, url, method, log=True, **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)
|
||||
|
||||
self.log_curl_request(url, method, 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 the option not to follow redirects
|
||||
follow_redirects = kwargs.pop('follow_redirects', 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
|
||||
|
||||
try:
|
||||
resp = super(SessionClient, self).request(url,
|
||||
method,
|
||||
headers=headers,
|
||||
data=data,
|
||||
**kwargs)
|
||||
except ksa_exc.ConnectTimeout as e:
|
||||
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
|
||||
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
|
||||
message = ("Error communicating with %(url)s %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except ksa_exc.ConnectFailure as e:
|
||||
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
|
||||
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
|
||||
resp = requests.request(
|
||||
method,
|
||||
self.endpoint_url + url,
|
||||
allow_redirects=allow_redirects,
|
||||
**kwargs)
|
||||
except socket.gaierror as e:
|
||||
message = ("Error finding address for %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
{'url': self.endpoint_url + url, 'e': e})
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except (socket.error,
|
||||
socket.timeout,
|
||||
requests.exceptions.ConnectionError) as e:
|
||||
endpoint = self.endpoint
|
||||
message = ("Error communicating with %(endpoint)s %(e)s" %
|
||||
{'endpoint': endpoint, 'e': e})
|
||||
raise exc.CommunicationError(message=message)
|
||||
|
||||
self.last_request_id = resp.headers.get('x-openstack-request-id')
|
||||
return self._handle_response(resp)
|
||||
if log:
|
||||
self.log_http_response(resp)
|
||||
|
||||
if 'X-Auth-Key' not in kwargs['headers'] and \
|
||||
(resp.status_code == 401 or
|
||||
(resp.status_code == 500 and
|
||||
"(HTTP 401)" in resp.content)):
|
||||
raise exc.HTTPUnauthorized("Authentication failed. Please try"
|
||||
" again.\n%s"
|
||||
% resp.content)
|
||||
elif 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
# Redirected. Reissue the request to the new location,
|
||||
# unless caller specified follow_redirects=False
|
||||
if follow_redirects:
|
||||
location = resp.headers.get('location')
|
||||
path = self.strip_endpoint(location)
|
||||
resp = self.request(path, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp)
|
||||
|
||||
return resp
|
||||
|
||||
def strip_endpoint(self, location):
|
||||
if location is None:
|
||||
message = "Location not returned with 302"
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
elif location.startswith(self.endpoint):
|
||||
return location[len(self.endpoint):]
|
||||
else:
|
||||
message = "Prohibited endpoint redirect %s" % location
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
|
||||
def credentials_headers(self):
|
||||
creds = {}
|
||||
if self.username:
|
||||
creds['X-Auth-User'] = self.username
|
||||
if self.password:
|
||||
creds['X-Auth-Key'] = self.password
|
||||
return creds
|
||||
|
||||
def json_request(self, url, method, **kwargs):
|
||||
params = _set_request_params(kwargs)
|
||||
resp = self.request(url, method, **params)
|
||||
return _handle_response(resp)
|
||||
|
||||
def json_patch_request(self, url, method='PATCH', **kwargs):
|
||||
return self.json_request(
|
||||
url, method, **kwargs)
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.json_request(url, "HEAD", **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.json_request(url, "GET", **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.json_request(url, "POST", **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.json_request(url, "PUT", **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.request(url, "DELETE", **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.json_request(url, "PATCH", **kwargs)
|
||||
|
||||
|
||||
def get_http_client(endpoint=None, session=None, **kwargs):
|
||||
class SessionClient(adapter.LegacyJsonAdapter):
|
||||
"""HTTP client based on Keystone client session."""
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
params = _set_request_params(kwargs)
|
||||
redirect = kwargs.get('redirect')
|
||||
|
||||
resp, body = super(SessionClient, self).request(
|
||||
url, method,
|
||||
**params)
|
||||
|
||||
if 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
if redirect:
|
||||
location = resp.headers.get('location')
|
||||
path = self.strip_endpoint(location)
|
||||
resp = self.request(path, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp)
|
||||
|
||||
if resp.headers.get('Content-Type') == 'application/octet-stream':
|
||||
body = _close_after_stream(resp, CHUNKSIZE)
|
||||
return resp, body
|
||||
|
||||
def strip_endpoint(self, location):
|
||||
if location is None:
|
||||
message = _("Location not returned with 302")
|
||||
raise exc.InvalidEndpoint(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(*args, **kwargs):
|
||||
session = kwargs.pop('session', None)
|
||||
auth = kwargs.pop('auth', None)
|
||||
endpoint = next(iter(args), None)
|
||||
|
||||
if session:
|
||||
return SessionClient(session, **kwargs)
|
||||
service_type = kwargs.pop('service_type', None)
|
||||
endpoint_type = kwargs.pop('endpoint_type', None)
|
||||
region_name = kwargs.pop('region_name', None)
|
||||
service_name = kwargs.pop('service_name', None)
|
||||
parameters = {
|
||||
'endpoint_override': endpoint,
|
||||
'session': session,
|
||||
'auth': auth,
|
||||
'interface': endpoint_type,
|
||||
'service_type': service_type,
|
||||
'region_name': region_name,
|
||||
'service_name': service_name,
|
||||
'user_agent': 'python-glareclient',
|
||||
}
|
||||
parameters.update(kwargs)
|
||||
return SessionClient(**parameters)
|
||||
elif endpoint:
|
||||
return HTTPClient(endpoint, **kwargs)
|
||||
else:
|
||||
|
@ -1,347 +0,0 @@
|
||||
# Copyright 2014 Red Hat, Inc
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
|
||||
import OpenSSL
|
||||
from requests import adapters
|
||||
from requests import compat
|
||||
try:
|
||||
from requests.packages.urllib3 import connectionpool
|
||||
from requests.packages.urllib3 import poolmanager
|
||||
except ImportError:
|
||||
from urllib3 import connectionpool
|
||||
from urllib3 import poolmanager
|
||||
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
import six
|
||||
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
|
||||
from six.moves import range
|
||||
|
||||
try:
|
||||
from eventlet import patcher
|
||||
# Handle case where we are running in a monkey patched environment
|
||||
if patcher.is_monkey_patched('socket'):
|
||||
from eventlet.green.httplib import HTTPSConnection
|
||||
from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
|
||||
else:
|
||||
raise ImportError
|
||||
except ImportError:
|
||||
from OpenSSL import SSL
|
||||
from six.moves import http_client
|
||||
HTTPSConnection = http_client.HTTPSConnection
|
||||
Connection = SSL.Connection
|
||||
|
||||
|
||||
from glareclient import exc
|
||||
|
||||
|
||||
def verify_callback(host=None):
|
||||
"""Provide wrapper for do_verify_callback.
|
||||
|
||||
We use a partial around the 'real' verify_callback function
|
||||
so that we can stash the host value without holding a
|
||||
reference on the VerifiedHTTPSConnection.
|
||||
"""
|
||||
def wrapper(connection, x509, errnum,
|
||||
depth, preverify_ok, host=host):
|
||||
return do_verify_callback(connection, x509, errnum,
|
||||
depth, preverify_ok, host=host)
|
||||
return wrapper
|
||||
|
||||
|
||||
def do_verify_callback(connection, x509, errnum,
|
||||
depth, preverify_ok, host=None):
|
||||
"""Verify the server's SSL certificate.
|
||||
|
||||
This is a standalone function rather than a method to avoid
|
||||
issues around closing sockets if a reference is held on
|
||||
a VerifiedHTTPSConnection by the callback function.
|
||||
"""
|
||||
if x509.has_expired():
|
||||
msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
|
||||
raise exc.SSLCertificateError(msg)
|
||||
|
||||
if depth == 0 and preverify_ok:
|
||||
# We verify that the host matches against the last
|
||||
# certificate in the chain
|
||||
return host_matches_cert(host, x509)
|
||||
else:
|
||||
# Pass through OpenSSL's default result
|
||||
return preverify_ok
|
||||
|
||||
|
||||
def host_matches_cert(host, x509):
|
||||
"""Verify the certificate identifies the host.
|
||||
|
||||
Verify that the x509 certificate we have received
|
||||
from 'host' correctly identifies the server we are
|
||||
connecting to, ie that the certificate's Common Name
|
||||
or a Subject Alternative Name matches 'host'.
|
||||
"""
|
||||
def check_match(name):
|
||||
# Directly match the name
|
||||
if name == host:
|
||||
return True
|
||||
|
||||
# Support single wildcard matching
|
||||
if name.startswith('*.') and host.find('.') > 0:
|
||||
if name[2:] == host.split('.', 1)[1]:
|
||||
return True
|
||||
|
||||
common_name = x509.get_subject().commonName
|
||||
|
||||
# First see if we can match the CN
|
||||
if check_match(common_name):
|
||||
return True
|
||||
# Also try Subject Alternative Names for a match
|
||||
san_list = None
|
||||
for i in range(x509.get_extension_count()):
|
||||
ext = x509.get_extension(i)
|
||||
if ext.get_short_name() == b'subjectAltName':
|
||||
san_list = str(ext)
|
||||
for san in ''.join(san_list.split()).split(','):
|
||||
if san.startswith('DNS:'):
|
||||
if check_match(san.split(':', 1)[1]):
|
||||
return True
|
||||
|
||||
# Server certificate does not match host
|
||||
msg = ('Host "%s" does not match x509 certificate contents: '
|
||||
'CommonName "%s"' % (host, common_name))
|
||||
if san_list is not None:
|
||||
msg = msg + ', subjectAltName "%s"' % san_list
|
||||
raise exc.SSLCertificateError(msg)
|
||||
|
||||
|
||||
def to_bytes(s):
|
||||
if isinstance(s, six.string_types):
|
||||
return six.b(s)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
class HTTPSAdapter(adapters.HTTPAdapter):
|
||||
"""This adapter will be used just when ssl compression should be disabled.
|
||||
|
||||
The init method overwrites the default https pool by setting
|
||||
glareclient's one.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
classes_by_scheme = poolmanager.pool_classes_by_scheme
|
||||
classes_by_scheme["glare+https"] = HTTPSConnectionPool
|
||||
super(HTTPSAdapter, self).__init__(*args, **kwargs)
|
||||
|
||||
def request_url(self, request, proxies):
|
||||
# NOTE(flaper87): Make sure the url is encoded, otherwise
|
||||
# python's standard httplib will fail with a TypeError.
|
||||
url = super(HTTPSAdapter, self).request_url(request, proxies)
|
||||
if six.PY2:
|
||||
url = encodeutils.safe_encode(url)
|
||||
return url
|
||||
|
||||
def _create_glare_httpsconnectionpool(self, url):
|
||||
kw = self.poolmanager.connection_pool_kw
|
||||
# Parse the url to get the scheme, host, and port
|
||||
parsed = compat.urlparse(url)
|
||||
# If there is no port specified, we should use the standard HTTPS port
|
||||
port = parsed.port or 443
|
||||
host = parsed.netloc.rsplit(':', 1)[0]
|
||||
pool = HTTPSConnectionPool(host, port, **kw)
|
||||
|
||||
with self.poolmanager.pools.lock:
|
||||
self.poolmanager.pools[(parsed.scheme, host, port)] = pool
|
||||
|
||||
return pool
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
try:
|
||||
return super(HTTPSAdapter, self).get_connection(url, proxies)
|
||||
except KeyError:
|
||||
# NOTE(sigamvirus24): This works around modifying a module global
|
||||
# which fixes bug #1396550
|
||||
# The scheme is most likely glare+https but check anyway
|
||||
if not url.startswith('glare+https://'):
|
||||
raise
|
||||
|
||||
return self._create_glare_httpsconnectionpool(url)
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
conn.ca_certs = verify[0]
|
||||
conn.insecure = verify[1]
|
||||
|
||||
|
||||
class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
|
||||
"""A replacement for the default HTTPSConnectionPool.
|
||||
|
||||
HTTPSConnectionPool will be instantiated when a new
|
||||
connection is requested to the HTTPSAdapter. This
|
||||
implementation overwrites the _new_conn method and
|
||||
returns an instances of glareclient's VerifiedHTTPSConnection
|
||||
which handles no compression.
|
||||
|
||||
ssl_compression is hard-coded to False because this will
|
||||
be used just when the user sets --no-ssl-compression.
|
||||
"""
|
||||
|
||||
scheme = 'glare+https'
|
||||
|
||||
def _new_conn(self):
|
||||
self.num_connections += 1
|
||||
return VerifiedHTTPSConnection(host=self.host,
|
||||
port=self.port,
|
||||
key_file=self.key_file,
|
||||
cert_file=self.cert_file,
|
||||
cacert=self.ca_certs,
|
||||
insecure=self.insecure,
|
||||
ssl_compression=False)
|
||||
|
||||
|
||||
class OpenSSLConnectionDelegator(object):
|
||||
"""An OpenSSL.SSL.Connection delegator.
|
||||
|
||||
Supplies an additional 'makefile' method which httplib requires
|
||||
and is not present in OpenSSL.SSL.Connection.
|
||||
|
||||
Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
|
||||
a delegator must be used.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.connection = Connection(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.connection, name)
|
||||
|
||||
def makefile(self, *args, **kwargs):
|
||||
return socket._fileobject(self.connection, *args, **kwargs)
|
||||
|
||||
|
||||
class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
"""Extended OpenSSL HTTPSConnection for enhanced SSL support.
|
||||
|
||||
Note: Much of this functionality can eventually be replaced
|
||||
with native Python 3.3 code.
|
||||
"""
|
||||
# Restrict the set of client supported cipher suites
|
||||
CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\
|
||||
'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\
|
||||
'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS'
|
||||
|
||||
def __init__(self, host, port=None, key_file=None, cert_file=None,
|
||||
cacert=None, timeout=None, insecure=False,
|
||||
ssl_compression=True):
|
||||
# List of exceptions reported by Python3 instead of
|
||||
# SSLConfigurationError
|
||||
if six.PY3:
|
||||
excp_lst = (TypeError, FileNotFoundError, ssl.SSLError)
|
||||
else:
|
||||
# NOTE(jamespage)
|
||||
# Accommodate changes in behaviour for pep-0467, introduced
|
||||
# in python 2.7.9.
|
||||
# https://github.com/python/peps/blob/master/pep-0476.txt
|
||||
excp_lst = (TypeError, IOError, ssl.SSLError)
|
||||
try:
|
||||
HTTPSConnection.__init__(self, host, port,
|
||||
key_file=key_file,
|
||||
cert_file=cert_file)
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.timeout = timeout
|
||||
self.insecure = insecure
|
||||
# NOTE(flaper87): `is_verified` is needed for
|
||||
# requests' urllib3. If insecure is True then
|
||||
# the request is not `verified`, hence `not insecure`
|
||||
self.is_verified = not insecure
|
||||
self.ssl_compression = ssl_compression
|
||||
self.cacert = None if cacert is None else str(cacert)
|
||||
self.set_context()
|
||||
# ssl exceptions are reported in various form in Python 3
|
||||
# so to be compatible, we report the same kind as under
|
||||
# Python2
|
||||
except excp_lst as e:
|
||||
raise exc.SSLConfigurationError(str(e))
|
||||
|
||||
def set_context(self):
|
||||
"""Set up the OpenSSL context."""
|
||||
self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||
self.context.set_cipher_list(self.CIPHERS)
|
||||
|
||||
if self.ssl_compression is False:
|
||||
self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
|
||||
|
||||
if self.insecure is not True:
|
||||
self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
|
||||
verify_callback(host=self.host))
|
||||
else:
|
||||
self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
|
||||
lambda *args: True)
|
||||
|
||||
if self.cert_file:
|
||||
try:
|
||||
self.context.use_certificate_file(self.cert_file)
|
||||
except Exception as e:
|
||||
msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
if self.key_file is None:
|
||||
# We support having key and cert in same file
|
||||
try:
|
||||
self.context.use_privatekey_file(self.cert_file)
|
||||
except Exception as e:
|
||||
msg = ('No key file specified and unable to load key '
|
||||
'from "%s" %s' % (self.cert_file, e))
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
|
||||
if self.key_file:
|
||||
try:
|
||||
self.context.use_privatekey_file(self.key_file)
|
||||
except Exception as e:
|
||||
msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
|
||||
if self.cacert:
|
||||
try:
|
||||
self.context.load_verify_locations(to_bytes(self.cacert))
|
||||
except Exception as e:
|
||||
msg = 'Unable to load CA from "%s" %s' % (self.cacert, e)
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
else:
|
||||
self.context.set_default_verify_paths()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to an SSL port using the OpenSSL library.
|
||||
|
||||
This method also applies per-connection parameters to the connection.
|
||||
"""
|
||||
result = socket.getaddrinfo(self.host, self.port, 0,
|
||||
socket.SOCK_STREAM)
|
||||
if result:
|
||||
socket_family = result[0][0]
|
||||
if socket_family == socket.AF_INET6:
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
# If due to some reason the address lookup fails - we still connect
|
||||
# to IPv4 socket. This retains the older behavior.
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if self.timeout is not None:
|
||||
# '0' microseconds
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
|
||||
struct.pack('LL', self.timeout, 0))
|
||||
self.sock = OpenSSLConnectionDelegator(self.context, sock)
|
||||
self.sock.connect((self.host, self.port))
|
48
glareclient/tests/unit/fakes.py
Normal file
48
glareclient/tests/unit/fakes.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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 oslo_serialization import jsonutils
|
||||
|
||||
|
||||
class FakeRaw(object):
|
||||
version = 110
|
||||
|
||||
|
||||
class FakeHTTPResponse(object):
|
||||
|
||||
version = 1.1
|
||||
|
||||
def __init__(self, status_code, reason, headers, content):
|
||||
self.headers = headers
|
||||
self.content = content
|
||||
self.text = content
|
||||
self.status_code = status_code
|
||||
self.reason = reason
|
||||
self.raw = FakeRaw()
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
return self.headers.get(name, default)
|
||||
|
||||
def getheaders(self):
|
||||
return self.headers.items()
|
||||
|
||||
def read(self, amt=None):
|
||||
b = self.content
|
||||
self.content = None
|
||||
return b
|
||||
|
||||
def iter_content(self, chunksize):
|
||||
return self.content
|
||||
|
||||
def json(self):
|
||||
return jsonutils.loads(self.content)
|
@ -1,55 +0,0 @@
|
||||
# Copyright 2014 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
from glareclient import client
|
||||
from glareclient import v1
|
||||
|
||||
|
||||
class ClientTest(testtools.TestCase):
|
||||
|
||||
def test_no_endpoint_error(self):
|
||||
self.assertRaises(ValueError, client.Client, None)
|
||||
|
||||
def test_endpoint(self):
|
||||
gc = client.Client('1', "http://example.com")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint(self):
|
||||
gc = client.Client('1', "http://example.com/v1")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_minor_revision(self):
|
||||
gc = client.Client('1', "http://example.com/v1.0")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_endpoint_with_version_hostname(self):
|
||||
gc = client.Client('1', "http://v1.example.com")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_version_hostname_v1(self):
|
||||
gc = client.Client(endpoint="http://v2.example.com/v1")
|
||||
self.assertEqual("http://v2.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_minor_revision_and_version_hostname(self):
|
||||
gc = client.Client(endpoint="http://v1.example.com/v1.1")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
483
glareclient/tests/unit/test_common_http.py
Normal file
483
glareclient/tests/unit/test_common_http.py
Normal file
@ -0,0 +1,483 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# 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
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from glareclient.common import exceptions as exc
|
||||
from glareclient.common import http
|
||||
from glareclient.tests.unit import fakes
|
||||
|
||||
|
||||
@mock.patch('glareclient.common.http.requests.request')
|
||||
class HttpClientTest(testtools.TestCase):
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setUp(self):
|
||||
super(HttpClientTest, self).setUp()
|
||||
|
||||
def test_http_raw_request(self, mock_request):
|
||||
headers = {'User-Agent': 'python-glareclient'}
|
||||
mock_request.return_value = \
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{},
|
||||
'')
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494')
|
||||
resp = client.request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('', ''.join([x for x in resp.content]))
|
||||
mock_request.assert_called_with('GET', 'http://example.com:9494',
|
||||
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:9494')
|
||||
resp = client.request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
client.username = 'user'
|
||||
client.password = 'pass'
|
||||
resp = client.request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
client.auth_token = 'abcd1234'
|
||||
resp = client.request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# no token or credentials
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
headers={'User-Agent': 'python-glareclient'}),
|
||||
mock.call('GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
headers={'User-Agent': 'python-glareclient',
|
||||
'X-Auth-Key': 'pass',
|
||||
'X-Auth-User': 'user'}),
|
||||
mock.call('GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
headers={'User-Agent': 'python-glareclient',
|
||||
'X-Auth-Token': 'abcd1234'})
|
||||
])
|
||||
|
||||
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:9494')
|
||||
client.region_name = 'RegionOne'
|
||||
resp = client.request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
mock_request.assert_called_once_with(
|
||||
'GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
headers={'X-Region-Name': 'RegionOne',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
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:9494')
|
||||
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:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
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:9494')
|
||||
client.verify_cert = True
|
||||
client.cert_file = 'RANDOM_CERT_FILE'
|
||||
client.key_file = 'RANDOM_KEY_FILE'
|
||||
client.auth_url = 'http://AUTH_URL'
|
||||
resp, body = client.json_request('', 'GET', data='text')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
|
||||
mock_request.assert_called_once_with(
|
||||
'GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
|
||||
verify=True,
|
||||
data='text',
|
||||
stream=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'X-Auth-Url': 'http://AUTH_URL',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
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:9494')
|
||||
resp, body = client.json_request('', 'GET', data='test-body')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
mock_request.assert_called_once_with(
|
||||
'GET', 'http://example.com:9494',
|
||||
data='test-body',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
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:9494')
|
||||
resp, body = client.json_request('', 'GET', data='test-data')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_called_once_with(
|
||||
'GET', 'http://example.com:9494', data='test-data',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
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:9494')
|
||||
resp, body = client.json_request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertIsNone(body)
|
||||
mock_request.assert_called_once_with(
|
||||
'GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
def test_http_manual_redirect_delete(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:9494/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')]
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494/foo')
|
||||
resp, body = client.json_request('', 'DELETE')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('DELETE', 'http://example.com:9494/foo',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'}),
|
||||
mock.call('DELETE', 'http://example.com:9494/foo/bar',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_post(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:9494/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')]
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494/foo')
|
||||
resp, body = client.json_request('', 'POST')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('POST', 'http://example.com:9494/foo',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'}),
|
||||
mock.call('POST', 'http://example.com:9494/foo/bar',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_put(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:9494/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')]
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494/foo')
|
||||
resp, body = client.json_request('', 'PUT')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('PUT', 'http://example.com:9494/foo',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'}),
|
||||
mock.call('PUT', 'http://example.com:9494/foo/bar',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_prohibited(self, mock_request):
|
||||
mock_request.return_value = \
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:9494/'},
|
||||
'')
|
||||
client = http.HTTPClient('http://example.com:9494/foo')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.json_request, '', 'DELETE')
|
||||
mock_request.assert_called_once_with(
|
||||
'DELETE', 'http://example.com:9494/foo',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
def test_http_manual_redirect_error_without_location(self, mock_request):
|
||||
mock_request.return_value = \
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{},
|
||||
'')
|
||||
client = http.HTTPClient('http://example.com:9494/foo')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.json_request, '', 'DELETE')
|
||||
mock_request.assert_called_once_with(
|
||||
'DELETE', 'http://example.com:9494/foo',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
def test_http_json_request_redirect(self, mock_request):
|
||||
# Record the 302
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:9494'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')]
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494')
|
||||
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:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'}),
|
||||
mock.call('GET', 'http://example.com:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
])
|
||||
|
||||
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:9494')
|
||||
e = self.assertRaises(exc.HTTPNotFound, 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:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
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:9494')
|
||||
e = self.assertRaises(
|
||||
exc.HTTPMultipleChoices, 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:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'})
|
||||
|
||||
def test_fake_json_request(self, mock_request):
|
||||
headers = {'User-Agent': 'python-glareclient'}
|
||||
mock_request.side_effect = [socket.gaierror]
|
||||
|
||||
client = http.HTTPClient('fake://example.com:9494')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.request, "/", "GET")
|
||||
mock_request.assert_called_once_with('GET', 'fake://example.com:9494/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
|
||||
def test_http_request_socket_error(self, mock_request):
|
||||
headers = {'User-Agent': 'python-glareclient'}
|
||||
mock_request.side_effect = [socket.gaierror]
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.request, "/", "GET")
|
||||
mock_request.assert_called_once_with('GET', 'http://example.com:9494/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
|
||||
def test_http_request_socket_timeout(self, mock_request):
|
||||
headers = {'User-Agent': 'python-glareclient'}
|
||||
mock_request.side_effect = [socket.timeout]
|
||||
|
||||
client = http.HTTPClient('http://example.com:9494')
|
||||
self.assertRaises(exc.CommunicationError,
|
||||
client.request, "/", "GET")
|
||||
mock_request.assert_called_once_with('GET', 'http://example.com:9494/',
|
||||
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:9494', 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:9494',
|
||||
allow_redirects=False,
|
||||
stream=False,
|
||||
data=None,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'User-Agent': 'python-glareclient'},
|
||||
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', cacert="NOWHERE")
|
||||
self.assertEqual("NOWHERE", client.verify_cert)
|
||||
|
||||
with mock.patch('glareclient.common.http.get_system_ca_file') as gsf:
|
||||
gsf.return_value = "SOMEWHERE"
|
||||
client = http.HTTPClient('https://foo')
|
||||
self.assertEqual("SOMEWHERE", client.verify_cert)
|
@ -1,401 +0,0 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# 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 functools
|
||||
import json
|
||||
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1 import token_endpoint
|
||||
|
||||
import mock
|
||||
import requests
|
||||
from requests_mock.contrib import fixture
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
|
||||
import testtools
|
||||
from testtools import matchers
|
||||
import types
|
||||
|
||||
import glareclient
|
||||
from glareclient.common import http
|
||||
from glareclient.tests import utils
|
||||
|
||||
|
||||
def original_only(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not hasattr(self.client, 'log_curl_request'):
|
||||
self.skipTest('Skip logging tests for session client')
|
||||
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
|
||||
class TestClient(testtools.TestCase):
|
||||
|
||||
scenarios = [
|
||||
('httpclient', {'create_client': '_create_http_client'}),
|
||||
('session', {'create_client': '_create_session_client'})
|
||||
]
|
||||
|
||||
def _create_http_client(self):
|
||||
return http.HTTPClient(self.endpoint, token=self.token)
|
||||
|
||||
def _create_session_client(self):
|
||||
auth = token_endpoint.Token(self.endpoint, self.token)
|
||||
sess = session.Session(auth=auth)
|
||||
return http.SessionClient(sess)
|
||||
|
||||
def setUp(self):
|
||||
super(TestClient, self).setUp()
|
||||
self.mock = self.useFixture(fixture.Fixture())
|
||||
|
||||
self.endpoint = 'http://example.com:9292'
|
||||
self.ssl_endpoint = 'https://example.com:9292'
|
||||
self.token = u'abc123'
|
||||
|
||||
self.client = getattr(self, self.create_client)()
|
||||
|
||||
def test_identity_headers_and_token(self):
|
||||
identity_headers = {
|
||||
'X-Auth-Token': 'auth_token',
|
||||
'X-User-Id': 'user',
|
||||
'X-Tenant-Id': 'tenant',
|
||||
'X-Roles': 'roles',
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': 'service_catalog',
|
||||
}
|
||||
# with token
|
||||
kwargs = {'token': u'fake-token',
|
||||
'identity_headers': identity_headers}
|
||||
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
|
||||
self.assertEqual('auth_token', http_client_object.auth_token)
|
||||
self.assertTrue(http_client_object.identity_headers.
|
||||
get('X-Auth-Token') is None)
|
||||
|
||||
def test_identity_headers_and_no_token_in_header(self):
|
||||
identity_headers = {
|
||||
'X-User-Id': 'user',
|
||||
'X-Tenant-Id': 'tenant',
|
||||
'X-Roles': 'roles',
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': 'service_catalog',
|
||||
}
|
||||
# without X-Auth-Token in identity headers
|
||||
kwargs = {'token': u'fake-token',
|
||||
'identity_headers': identity_headers}
|
||||
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
|
||||
self.assertEqual(u'fake-token', http_client_object.auth_token)
|
||||
self.assertTrue(http_client_object.identity_headers.
|
||||
get('X-Auth-Token') is None)
|
||||
|
||||
def test_identity_headers_and_no_token_in_session_header(self):
|
||||
# Tests that if token or X-Auth-Token are not provided in the kwargs
|
||||
# when creating the http client, the session headers don't contain
|
||||
# the X-Auth-Token key.
|
||||
identity_headers = {
|
||||
'X-User-Id': 'user',
|
||||
'X-Tenant-Id': 'tenant',
|
||||
'X-Roles': 'roles',
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': 'service_catalog',
|
||||
}
|
||||
kwargs = {'identity_headers': identity_headers}
|
||||
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
|
||||
self.assertIsNone(http_client_object.auth_token)
|
||||
self.assertNotIn('X-Auth-Token', http_client_object.session.headers)
|
||||
|
||||
def test_identity_headers_are_passed(self):
|
||||
# Tests that if token or X-Auth-Token are not provided in the kwargs
|
||||
# when creating the http client, the session headers don't contain
|
||||
# the X-Auth-Token key.
|
||||
identity_headers = {
|
||||
'X-User-Id': b'user',
|
||||
'X-Tenant-Id': b'tenant',
|
||||
'X-Roles': b'roles',
|
||||
'X-Identity-Status': b'Confirmed',
|
||||
'X-Service-Catalog': b'service_catalog',
|
||||
}
|
||||
kwargs = {'identity_headers': identity_headers}
|
||||
http_client = http.HTTPClient(self.endpoint, **kwargs)
|
||||
|
||||
path = '/artifactsmy-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
for k, v in six.iteritems(identity_headers):
|
||||
self.assertEqual(v, headers[k])
|
||||
|
||||
def test_language_header_passed(self):
|
||||
kwargs = {'language_header': 'nb_NO'}
|
||||
http_client = http.HTTPClient(self.endpoint, **kwargs)
|
||||
|
||||
path = '/v2/images/my-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(kwargs['language_header'], headers['Accept-Language'])
|
||||
|
||||
def test_language_header_not_passed_no_language(self):
|
||||
kwargs = {}
|
||||
http_client = http.HTTPClient(self.endpoint, **kwargs)
|
||||
|
||||
path = '/v2/images/my-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertTrue('Accept-Language' not in headers)
|
||||
|
||||
def test_connection_timeout(self):
|
||||
"""Verify a InvalidEndpoint is received if connection times out."""
|
||||
def cb(request, context):
|
||||
raise requests.exceptions.Timeout
|
||||
|
||||
path = '/v1/images'
|
||||
self.mock.get(self.endpoint + path, text=cb)
|
||||
comm_err = self.assertRaises(glareclient.exc.InvalidEndpoint,
|
||||
self.client.get,
|
||||
'/v1/images')
|
||||
self.assertIn(self.endpoint, comm_err.message)
|
||||
|
||||
def test_connection_refused(self):
|
||||
"""Verify a CommunicationError is received if connection is refused.
|
||||
|
||||
The error should list the host and port that refused the connection.
|
||||
"""
|
||||
def cb(request, context):
|
||||
raise requests.exceptions.ConnectionError()
|
||||
|
||||
path = '/artifacts/?limit=20'
|
||||
self.mock.get(self.endpoint + path, text=cb)
|
||||
|
||||
comm_err = self.assertRaises(glareclient.exc.CommunicationError,
|
||||
self.client.get,
|
||||
'/artifacts/?limit=20')
|
||||
|
||||
self.assertIn(self.endpoint, comm_err.message)
|
||||
|
||||
def test_http_encoding(self):
|
||||
path = '/artifacts/'
|
||||
text = 'Ok'
|
||||
self.mock.get(self.endpoint + path, text=text,
|
||||
headers={"Content-Type": "text/plain"})
|
||||
|
||||
headers = {"test": u'ni\xf1o'}
|
||||
resp, body = self.client.get(path, headers=headers)
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
def test_request_id(self):
|
||||
path = '/artifacts/'
|
||||
self.mock.get(self.endpoint + path,
|
||||
headers={"x-openstack-request-id": "req-aaa"})
|
||||
|
||||
self.client.get(path)
|
||||
self.assertEqual(self.client.last_request_id, 'req-aaa')
|
||||
|
||||
def test_headers_encoding(self):
|
||||
value = u'ni\xf1o'
|
||||
headers = {"test": value, "none-val": None}
|
||||
encoded = http.encode_headers(headers)
|
||||
self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"])
|
||||
self.assertNotIn("none-val", encoded)
|
||||
|
||||
def test_raw_request(self):
|
||||
"""Verify the path being used for HTTP requests reflects accurately."""
|
||||
headers = {"Content-Type": "text/plain"}
|
||||
text = 'Ok'
|
||||
path = '/artifacts/'
|
||||
|
||||
self.mock.get(self.endpoint + path, text=text, headers=headers)
|
||||
|
||||
resp, body = self.client.get('/artifacts/', headers=headers)
|
||||
self.assertEqual(headers, resp.headers)
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
def test_parse_endpoint(self):
|
||||
endpoint = 'http://example.com:9292'
|
||||
test_client = http.HTTPClient(endpoint, token=u'adc123')
|
||||
actual = test_client.parse_endpoint(endpoint)
|
||||
expected = parse.SplitResult(scheme='http',
|
||||
netloc='example.com:9292', path='',
|
||||
query='', fragment='')
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_get_connections_kwargs_http(self):
|
||||
endpoint = 'http://example.com:9292'
|
||||
test_client = http.HTTPClient(endpoint, token=u'adc123')
|
||||
self.assertEqual(600.0, test_client.timeout)
|
||||
|
||||
def test__chunk_body_exact_size_chunk(self):
|
||||
test_client = http._BaseHTTPClient()
|
||||
bytestring = b'x' * http.CHUNKSIZE
|
||||
data = six.BytesIO(bytestring)
|
||||
chunk = list(test_client._chunk_body(data))
|
||||
self.assertEqual(1, len(chunk))
|
||||
self.assertEqual([bytestring], chunk)
|
||||
|
||||
def test_http_chunked_request(self):
|
||||
text = "Ok"
|
||||
data = six.StringIO(text)
|
||||
path = '/artifacts'
|
||||
self.mock.post(self.endpoint + path, text=text)
|
||||
|
||||
headers = {"test": u'chunked_request'}
|
||||
resp, body = self.client.post(path, headers=headers, data=data)
|
||||
self.assertIsInstance(self.mock.last_request.body, types.GeneratorType)
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
def test_http_json(self):
|
||||
data = {"test": "json_request"}
|
||||
path = '/artifacts'
|
||||
text = 'OK'
|
||||
self.mock.post(self.endpoint + path, text=text)
|
||||
|
||||
headers = {"test": u'chunked_request'}
|
||||
resp, body = self.client.post(path, headers=headers, data=data)
|
||||
|
||||
self.assertEqual(text, resp.text)
|
||||
self.assertIsInstance(self.mock.last_request.body, six.string_types)
|
||||
self.assertEqual(data, json.loads(self.mock.last_request.body))
|
||||
|
||||
@original_only
|
||||
def test_log_http_response_with_non_ascii_char(self):
|
||||
try:
|
||||
response = 'Ok'
|
||||
headers = {"Content-Type": "text/plain",
|
||||
"test": "value1\xa5\xa6"}
|
||||
fake = utils.FakeResponse(headers, six.StringIO(response))
|
||||
self.client.log_http_response(fake)
|
||||
except UnicodeDecodeError as e:
|
||||
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
|
||||
|
||||
@original_only
|
||||
def test_log_curl_request_with_non_ascii_char(self):
|
||||
try:
|
||||
headers = {'header1': 'value1\xa5\xa6'}
|
||||
body = 'examplebody\xa5\xa6'
|
||||
self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body,
|
||||
None)
|
||||
except UnicodeDecodeError as e:
|
||||
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
|
||||
|
||||
@original_only
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_body_and_header(self, mock_log):
|
||||
hd_name = 'header1'
|
||||
hd_val = 'value1'
|
||||
headers = {hd_name: hd_val}
|
||||
body = 'examplebody'
|
||||
self.client.log_curl_request('GET', '/api/v1/', headers, body, None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
hd_regex = ".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val)
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex(hd_regex),
|
||||
'header not found in curl command')
|
||||
body_regex = ".*\s-d\s+'%s'\s.*" % body
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex(body_regex),
|
||||
'body not found in curl command')
|
||||
|
||||
def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert):
|
||||
headers = {'header1': 'value1'}
|
||||
http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key,
|
||||
cert_file=cert, cacert=cacert,
|
||||
token='fake-token')
|
||||
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
|
||||
None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
|
||||
needles = {'key': key, 'cert': cert, 'cacert': cacert}
|
||||
for option, value in six.iteritems(needles):
|
||||
if value:
|
||||
regex = ".*\s--%s\s+('%s'|%s).*" % (option, value, value)
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex(regex),
|
||||
'no --%s option in curl command' % option)
|
||||
else:
|
||||
regex = ".*\s--%s\s+.*" % option
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.Not(matchers.MatchesRegex(regex)),
|
||||
'unexpected --%s option in curl command' %
|
||||
option)
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_all_certs(self, mock_log):
|
||||
self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1',
|
||||
'cacert2')
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_some_certs(self, mock_log):
|
||||
self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None)
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_insecure_param(self, mock_log):
|
||||
headers = {'header1': 'value1'}
|
||||
http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True,
|
||||
token='fake-token')
|
||||
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
|
||||
None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex('.*\s-k\s.*'),
|
||||
'no -k option in curl command')
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_token_header(self, mock_log):
|
||||
fake_token = 'fake-token'
|
||||
headers = {'X-Auth-Token': fake_token}
|
||||
http_client_object = http.HTTPClient(self.endpoint,
|
||||
identity_headers=headers)
|
||||
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
|
||||
None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
token_regex = '.*%s.*' % fake_token
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.Not(matchers.MatchesRegex(token_regex)),
|
||||
'token found in LOG.debug parameter')
|
||||
|
||||
def test_expired_token_has_changed(self):
|
||||
# instantiate client with some token
|
||||
fake_token = b'fake-token'
|
||||
http_client = http.HTTPClient(self.endpoint,
|
||||
token=fake_token)
|
||||
path = '/artifacts'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(fake_token, headers['X-Auth-Token'])
|
||||
# refresh the token
|
||||
refreshed_token = b'refreshed-token'
|
||||
http_client.auth_token = refreshed_token
|
||||
http_client.get(path)
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(refreshed_token, headers['X-Auth-Token'])
|
||||
# regression check for bug 1448080
|
||||
unicode_token = u'ni\xf1o'
|
||||
http_client.auth_token = unicode_token
|
||||
http_client.get(path)
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(b'ni\xc3\xb1o', headers['X-Auth-Token'])
|
@ -1,226 +0,0 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# 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 os
|
||||
|
||||
import mock
|
||||
import six
|
||||
import ssl
|
||||
import testtools
|
||||
import threading
|
||||
|
||||
from glareclient import Client
|
||||
from glareclient import exc
|
||||
from glareclient import v1
|
||||
|
||||
if six.PY3 is True:
|
||||
import socketserver
|
||||
else:
|
||||
import SocketServer as socketserver
|
||||
|
||||
|
||||
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'var'))
|
||||
|
||||
|
||||
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
self.request.recv(1024)
|
||||
response = b'somebytes'
|
||||
self.request.sendall(response)
|
||||
|
||||
|
||||
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
def get_request(self):
|
||||
key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
|
||||
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
(_sock, addr) = socketserver.TCPServer.get_request(self)
|
||||
sock = ssl.wrap_socket(_sock,
|
||||
certfile=cert_file,
|
||||
keyfile=key_file,
|
||||
ca_certs=cacert,
|
||||
server_side=True,
|
||||
cert_reqs=ssl.CERT_REQUIRED)
|
||||
return sock, addr
|
||||
|
||||
|
||||
class TestHTTPSVerifyCert(testtools.TestCase):
|
||||
"""Check 'requests' based ssl verification occurs.
|
||||
|
||||
The requests library performs SSL certificate validation,
|
||||
however there is still a need to check that the glare
|
||||
client is properly integrated with requests so that
|
||||
cert validation actually happens.
|
||||
"""
|
||||
def setUp(self):
|
||||
# Rather than spinning up a new process, we create
|
||||
# a thread to perform client/server interaction.
|
||||
# This should run more quickly.
|
||||
super(TestHTTPSVerifyCert, self).setUp()
|
||||
server = ThreadedTCPServer(('127.0.0.1', 0),
|
||||
ThreadedTCPRequestHandler)
|
||||
__, self.port = server.server_address
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_cert_verification(self, __):
|
||||
"""v1 regression test for bug 115260."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
|
||||
try:
|
||||
client = v1.Client(url,
|
||||
insecure=False,
|
||||
ssl_compression=True)
|
||||
client.artifacts.get('123', type_name='sample_artifact')
|
||||
self.fail('No SSL exception has been raised')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' not in e.message:
|
||||
self.fail('No certificate failure message is received')
|
||||
except Exception:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_cert_verification_no_compression(self, __):
|
||||
"""v1 regression test for bug 115260."""
|
||||
# Legacy test. Verify 'no compression' has no effect
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
|
||||
try:
|
||||
client = v1.Client(url,
|
||||
insecure=False,
|
||||
ssl_compression=False)
|
||||
client.artifacts.get('123', type_name='sample_artifact')
|
||||
self.fail('No SSL exception has been raised')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' not in e.message:
|
||||
self.fail('No certificate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_valid_cert_verification(self, __):
|
||||
"""Test absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=True,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' in e.message:
|
||||
self.fail('Certificate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_valid_cert_verification_no_compression(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' in e.message:
|
||||
self.fail('Certificate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_valid_cert_no_key(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cert_file=cert_file,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
if ('PEM lib' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_bad_cert(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt')
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cert_file=cert_file,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
# NOTE(dsariel)
|
||||
# starting from python 2.7.8 the way to handle loading private
|
||||
# keys into the SSL_CTX was changed and error message become
|
||||
# similar to the one in 3.X
|
||||
if (six.PY2 and 'PrivateKey' not in e.message and
|
||||
'PEM lib' not in e.message or
|
||||
six.PY3 and 'PEM lib' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_bad_ca(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'badca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
# NOTE(dsariel)
|
||||
# starting from python 2.7.8 the way of handling x509 certificates
|
||||
# was changed (github.com/python/peps/blob/master/pep-0476.txt#L28)
|
||||
# and error message become similar to the one in 3.X
|
||||
if (six.PY2 and 'certificate' not in e.message and
|
||||
'No such file' not in e.message or
|
||||
six.PY3 and 'No such file' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
@ -1,3 +1,4 @@
|
||||
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
@ -13,28 +14,22 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from glareclient.common import http
|
||||
from glareclient.common import utils
|
||||
from glareclient.v1 import artifacts
|
||||
from glareclient.v1 import versions
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the Glare Artifact Repository v2 API.
|
||||
"""Client for the Glare Artifact Repository v1 API.
|
||||
|
||||
:param string endpoint: A user-supplied endpoint URL for the glare
|
||||
service.
|
||||
:param string endpoint: A user-supplied endpoint URL for the glare service.
|
||||
:param string token: Token for authentication.
|
||||
:param integer timeout: Allows customization of the timeout for client
|
||||
http requests. (optional)
|
||||
:param string language_header: Set Accept-Language header to be sent in
|
||||
requests to glare.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint=None, **kwargs):
|
||||
endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0)
|
||||
self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
"""Initialize a new client for the Glare v1 API."""
|
||||
|
||||
self.version = kwargs.get('version')
|
||||
self.http_client = http.construct_http_client(endpoint, **kwargs)
|
||||
self.artifacts = artifacts.Controller(self.http_client)
|
||||
self.versions = versions.VersionController(self.http_client)
|
||||
|
@ -9,4 +9,5 @@ requests>=2.10.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
oslo.utils>=3.16.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.log>=3.11.0 # Apache-2.0
|
||||
osc-lib>=1.0.2 # Apache-2.0
|
||||
|
Loading…
Reference in New Issue
Block a user