summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAmalaBasha <amala.alungal@RACKSPACE.COM>2014-07-01 14:45:12 +0530
committerAmalaBasha <amala.alungal@RACKSPACE.COM>2014-07-10 13:22:05 +0530
commitdbb242b776908ca50ed8557ebfe7cfcd879366c8 (patch)
tree597e583588dfb4313cf9d10bca34cc956c716e9f
parent1db17aaad9a04cc98fc848b23a76db52a5f62965 (diff)
downloadpython-glanceclient-dbb242b776908ca50ed8557ebfe7cfcd879366c8.tar.gz
Replace old httpclient with requests
This review implements blueprint python-request and replaces the old http client implementation in favor of a new one based on python-requests. Major changes: * raw_request and json_request removed since everything is now being handled by the same method "_request" * New methods that match HTTP's methods were added: - get - put - post - head - patch - delete * Content-Type is now being "inferred" based on the data being sent: - if it is file-like object it chunks the request - if it is a python type not instance of basestring then it'll try to serialize it to json - Every other case will keep the incoming content-type and will send the data as is. * Glanceclient's HTTPSConnection implementation will be used if no-compression flag is set to True. Co-Author: Flavio Percoco<flaper87@gmail.com> Change-Id: I09f70eee3e2777f52ce040296015d41649c2586a
-rw-r--r--glanceclient/common/http.py576
-rw-r--r--glanceclient/common/https.py274
-rw-r--r--glanceclient/common/utils.py20
-rw-r--r--glanceclient/exc.py2
-rw-r--r--glanceclient/v1/client.py14
-rw-r--r--glanceclient/v1/image_members.py10
-rw-r--r--glanceclient/v1/images.py49
-rw-r--r--glanceclient/v2/image_members.py13
-rw-r--r--glanceclient/v2/image_tags.py4
-rw-r--r--glanceclient/v2/images.py37
-rw-r--r--glanceclient/v2/schemas.py2
-rw-r--r--requirements.txt1
-rw-r--r--tests/test_exc.py10
-rw-r--r--tests/test_http.py389
-rw-r--r--tests/test_shell.py25
-rw-r--r--tests/test_ssl.py93
-rw-r--r--tests/utils.py126
-rw-r--r--tests/v1/test_images.py2
-rw-r--r--tests/v1/test_legacy_shell.py2
-rw-r--r--tests/v1/test_shell.py32
-rw-r--r--tests/v2/test_images.py4
-rw-r--r--tests/v2/test_shell_v2.py80
22 files changed, 723 insertions, 1042 deletions
diff --git a/glanceclient/common/http.py b/glanceclient/common/http.py
index 84714df..a990be5 100644
--- a/glanceclient/common/http.py
+++ b/glanceclient/common/http.py
@@ -14,16 +14,11 @@
# under the License.
import copy
-import errno
-import hashlib
import logging
-import posixpath
import socket
-import ssl
-import struct
+import requests
import six
-from six.moves import http_client
from six.moves.urllib import parse
try:
@@ -36,9 +31,7 @@ if not hasattr(parse, 'parse_qsl'):
import cgi
parse.parse_qsl = cgi.parse_qsl
-import OpenSSL
-
-from glanceclient.common import utils
+from glanceclient.common import https
from glanceclient import exc
from glanceclient.openstack.common import importutils
from glanceclient.openstack.common import network_utils
@@ -46,48 +39,15 @@ from glanceclient.openstack.common import strutils
osprofiler_web = importutils.try_import("osprofiler.web")
-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
- from eventlet.greenio import GreenSocket
- # TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
- GreenSocket.getsockopt = utils.getsockopt
- else:
- raise ImportError
-except ImportError:
- HTTPSConnection = http_client.HTTPSConnection
- from OpenSSL.SSL import Connection as Connection
-
-
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-glanceclient'
CHUNKSIZE = 1024 * 64 # 64kB
-def to_bytes(s):
- if isinstance(s, six.string_types):
- return six.b(s)
- else:
- return s
-
-
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
- endpoint_parts = self.parse_endpoint(self.endpoint)
- self.endpoint_scheme = endpoint_parts.scheme
- self.endpoint_hostname = endpoint_parts.hostname
- self.endpoint_port = endpoint_parts.port
- self.endpoint_path = endpoint_parts.path
-
- self.connection_class = self.get_connection_class(self.endpoint_scheme)
- self.connection_kwargs = self.get_connection_kwargs(
- self.endpoint_scheme, **kwargs)
-
self.identity_headers = kwargs.get('identity_headers')
self.auth_token = kwargs.get('token')
if self.identity_headers:
@@ -95,71 +55,58 @@ class HTTPClient(object):
self.auth_token = self.identity_headers.get('X-Auth-Token')
del self.identity_headers['X-Auth-Token']
- @staticmethod
- def parse_endpoint(endpoint):
- return network_utils.urlsplit(endpoint)
+ self.session = requests.Session()
+ self.session.headers["User-Agent"] = USER_AGENT
+ self.session.headers["X-Auth-Token"] = self.auth_token
- @staticmethod
- def get_connection_class(scheme):
- if scheme == 'https':
- return VerifiedHTTPSConnection
- else:
- return http_client.HTTPConnection
+ self.timeout = float(kwargs.get('timeout', 600))
- @staticmethod
- def get_connection_kwargs(scheme, **kwargs):
- _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
+ if self.endpoint.startswith("https"):
+ compression = kwargs.get('ssl_compression', True)
- if scheme == 'https':
- _kwargs['cacert'] = kwargs.get('cacert', None)
- _kwargs['cert_file'] = kwargs.get('cert_file', None)
- _kwargs['key_file'] = kwargs.get('key_file', None)
- _kwargs['insecure'] = kwargs.get('insecure', False)
- _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
+ if not compression:
+ self.session.mount("https://", https.HTTPSAdapter())
- return _kwargs
+ self.session.verify = kwargs.get('cacert',
+ not kwargs.get('insecure', True))
+ self.session.cert = (kwargs.get('cert_file'),
+ kwargs.get('key_file'))
- def get_connection(self):
- _class = self.connection_class
- try:
- return _class(self.endpoint_hostname, self.endpoint_port,
- **self.connection_kwargs)
- except http_client.InvalidURL:
- raise exc.InvalidEndpoint()
+ @staticmethod
+ def parse_endpoint(endpoint):
+ return network_utils.urlsplit(endpoint)
- def log_curl_request(self, method, url, kwargs):
+ def log_curl_request(self, method, url, headers, data, kwargs):
curl = ['curl -i -X %s' % method]
- for (key, value) in kwargs['headers'].items():
+ for (key, value) in self.session.headers.items():
if key.lower() == 'x-auth-token':
value = '*' * 3
header = '-H \'%s: %s\'' % (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.connection_kwargs.get(key)
- if value:
- curl.append(fmt % value)
-
- if self.connection_kwargs.get('insecure'):
+ curl.append(strutils.safe_encode(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 kwargs.get('body') is not None:
- curl.append('-d \'%s\'' % kwargs['body'])
+ if data and isinstance(data, six.string_types):
+ curl.append('-d \'%s\'' % data)
- curl.append('%s%s' % (self.endpoint, url))
+ if "//:" not in url:
+ url = '%s%s' % (self.endpoint, url)
+ curl.append(url)
LOG.debug(strutils.safe_encode(' '.join(curl), errors='ignore'))
@staticmethod
def log_http_response(resp, body=None):
- status = (resp.version / 10.0, resp.status, resp.reason)
+ status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
- headers = resp.getheaders()
+ headers = resp.headers.items()
if 'X-Auth-Token' in headers:
headers['X-Auth-Token'] = '*' * 3
dump.extend(['%s: %s' % (k, v) for k, v in headers])
@@ -183,69 +130,59 @@ class HTTPClient(object):
return dict((strutils.safe_encode(h), strutils.safe_encode(v))
for h, v in six.iteritems(headers))
- def _http_request(self, url, method, **kwargs):
+ 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
- kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
- kwargs['headers'].setdefault('User-Agent', USER_AGENT)
+ headers = kwargs.pop("headers", {})
+ headers = headers and copy.deepcopy(headers) or {}
- if osprofiler_web:
- kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
+ # Default Content-Type is octet-stream
+ content_type = headers.get('Content-Type', 'application/octet-stream')
- if self.auth_token:
- kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
+ def chunk_body(body):
+ chunk = body
+ while chunk:
+ chunk = body.read(CHUNKSIZE)
+ yield chunk
- if self.identity_headers:
- for k, v in six.iteritems(self.identity_headers):
- kwargs['headers'].setdefault(k, v)
+ data = kwargs.pop("data", None)
+ if data is not None and not isinstance(data, six.string_types):
+ try:
+ data = json.dumps(data)
+ content_type = 'application/json'
+ except TypeError:
+ # Here we assume it's
+ # a file-like object
+ # and we'll chunk it
+ data = chunk_body(data)
- self.log_curl_request(method, url, kwargs)
- conn = self.get_connection()
+ headers['Content-Type'] = content_type
# Note(flaper87): Before letting headers / url fly,
# they should be encoded otherwise httplib will
- # complain. If we decide to rely on python-request
- # this wont be necessary anymore.
- kwargs['headers'] = self.encode_headers(kwargs['headers'])
+ # complain.
+ headers = self.encode_headers(headers)
try:
- if self.endpoint_path:
- # NOTE(yuyangbj): this method _http_request could either be
- # called by API layer, or be called recursively with
- # redirection. For example, url would be '/v1/images/detail'
- # from API layer, but url would be 'https://example.com:92/
- # v1/images/detail' from recursion.
- # See bug #1230032 and bug #1208618.
- if url is not None:
- all_parts = parse.urlparse(url)
- if not (all_parts.scheme and all_parts.netloc):
- norm_parse = posixpath.normpath
- url = norm_parse('/'.join([self.endpoint_path, url]))
- else:
- url = self.endpoint_path
-
- conn_url = parse.urlsplit(url).geturl()
- # Note(flaper87): Ditto, headers / url
- # encoding to make httplib happy.
- conn_url = strutils.safe_encode(conn_url)
- if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
- conn.putrequest(method, conn_url)
- for header, value in kwargs['headers'].items():
- conn.putheader(header, value)
- conn.endheaders()
- chunk = kwargs['body'].read(CHUNKSIZE)
- # Chunk it, baby...
- while chunk:
- conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
- chunk = kwargs['body'].read(CHUNKSIZE)
- conn.send('0\r\n\r\n')
- else:
- conn.request(method, conn_url, **kwargs)
- resp = conn.getresponse()
+ conn_url = "%s/%s" % (self.endpoint, url)
+ self.log_curl_request(method, conn_url, headers, data, kwargs)
+ resp = self.session.request(method,
+ conn_url,
+ data=data,
+ stream=True,
+ headers=headers,
+ **kwargs)
+ except requests.exceptions.Timeout as e:
+ message = ("Error communicating with %(endpoint)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)
@@ -256,357 +193,46 @@ class HTTPClient(object):
{'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message)
- body_iter = ResponseBodyIterator(resp)
-
- # Read body into string if it isn't obviously image data
- if resp.getheader('content-type', None) != 'application/octet-stream':
- body_str = b''.join([to_bytes(chunk) for chunk in body_iter])
- self.log_http_response(resp, body_str)
- body_iter = six.BytesIO(body_str)
- else:
- self.log_http_response(resp)
-
- if 400 <= resp.status < 600:
- LOG.debug("Request returned failure status: %d" % resp.status)
- raise exc.from_response(resp, body_str)
- elif resp.status in (301, 302, 305):
- # Redirected. Reissue the request to the new location.
- return self._http_request(resp.getheader('location', None), method,
- **kwargs)
- elif resp.status == 300:
+ if not resp.ok:
+ LOG.error("Request returned failure status %s." % resp.status_code)
+ raise exc.from_response(resp, resp.content)
+ elif resp.status_code == requests.codes.MULTIPLE_CHOICES:
raise exc.from_response(resp)
- return resp, body_iter
-
- def json_request(self, method, url, **kwargs):
- kwargs.setdefault('headers', {})
- kwargs['headers'].setdefault('Content-Type', 'application/json')
-
- if 'body' in kwargs:
- kwargs['body'] = json.dumps(kwargs['body'])
-
- resp, body_iter = self._http_request(url, method, **kwargs)
-
- if 'application/json' in resp.getheader('content-type', ''):
- body = ''.join([chunk for chunk in body_iter])
- try:
- body = json.loads(body)
- except ValueError:
- LOG.error('Could not decode response body as JSON')
- else:
- body = None
-
- return resp, body
+ content_type = resp.headers.get('Content-Type')
- def raw_request(self, method, url, **kwargs):
- kwargs.setdefault('headers', {})
- kwargs['headers'].setdefault('Content-Type',
- 'application/octet-stream')
-
- if 'content_length' in kwargs:
- content_length = kwargs.pop('content_length')
+ # Read body into string if it isn't obviously image data
+ if content_type == 'application/octet-stream':
+ # Do not read all response in memory when
+ # downloading an image.
+ body_iter = resp.iter_content(chunk_size=CHUNKSIZE)
+ self.log_http_response(resp)
else:
- content_length = None
-
- if (('body' in kwargs) and (hasattr(kwargs['body'], 'read') and
- method.lower() in ('post', 'put'))):
-
- # NOTE(dosaboy): only use chunked transfer if not setting a
- # content length since setting it will implicitly disable
- # chunking.
-
- file_content_length = utils.get_file_size(kwargs['body'])
- if content_length is None:
- content_length = file_content_length
- elif (file_content_length and
- (content_length != file_content_length)):
- errmsg = ("supplied content-length (%s) does not match "
- "length of supplied data (%s)" %
- (content_length, file_content_length))
- raise AttributeError(errmsg)
-
- if content_length is None:
- # We use 'Transfer-Encoding: chunked' because
- # body size may not always be known in advance.
- kwargs['headers']['Transfer-Encoding'] = 'chunked'
+ content = resp.content
+ self.log_http_response(resp, content)
+ if content_type and content_type.startswith('application/json'):
+ # Let's use requests json method,
+ # it should take care of response
+ # encoding
+ body_iter = resp.json()
else:
- kwargs['headers']['Content-Length'] = str(content_length)
-
- return self._http_request(url, method, **kwargs)
-
- def client_request(self, method, url, **kwargs):
- # NOTE(akurilin): this method provides compatibility with methods which
- # expects requests.Response object(for example - methods of
- # class Managers from common code).
- if 'json' in kwargs and 'body' not in kwargs:
- kwargs['body'] = kwargs.pop('json')
- resp, body = self.json_request(method, url, **kwargs)
- resp.json = lambda: body
- resp.content = bool(body)
- resp.status_code = resp.status
- return resp
+ body_iter = six.StringIO(content)
+ return resp, body_iter
def head(self, url, **kwargs):
- return self.client_request("HEAD", url, **kwargs)
+ return self._request('HEAD', url, **kwargs)
def get(self, url, **kwargs):
- return self.client_request("GET", url, **kwargs)
+ return self._request('GET', url, **kwargs)
def post(self, url, **kwargs):
- return self.client_request("POST", url, **kwargs)
+ return self._request('POST', url, **kwargs)
def put(self, url, **kwargs):
- return self.client_request("PUT", url, **kwargs)
-
- def delete(self, url, **kwargs):
- return self.raw_request("DELETE", url, **kwargs)
+ return self._request('PUT', url, **kwargs)
def patch(self, url, **kwargs):
- return self.client_request("PATCH", url, **kwargs)
-
-
-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):
- # Making sure socket is closed when this file is closed
- # since we now avoid closing socket on connection close
- # see new close method under VerifiedHTTPSConnection
- kwargs['close'] = True
-
- return socket._fileobject(self.connection, *args, **kwargs)
-
-
-class VerifiedHTTPSConnection(HTTPSConnection):
- """
- Extended HTTPSConnection which uses the OpenSSL library
- for enhanced SSL support.
- Note: Much of this functionality can eventually be replaced
- with native Python 3.3 code.
- """
- 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:
- excp_lst = ()
- 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
- self.ssl_compression = ssl_compression
- self.cacert = None if cacert is None else str(cacert)
- self.setcontext()
- # 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))
-
- @staticmethod
- def host_matches_cert(host, x509):
- """
- Verify that the x509 certificate we have received
- from 'host' correctly identifies the server we are
- connecting to, i.e. 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 verify_callback(self, connection, x509, errnum,
- depth, preverify_ok):
- # NOTE(leaman): preverify_ok may be a non-boolean type
- preverify_ok = bool(preverify_ok)
- 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 self.host_matches_cert(self.host, x509)
- else:
- # Pass through OpenSSL's default result
- return preverify_ok
-
- def setcontext(self):
- """
- Set up the OpenSSL context.
- """
- self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
-
- if self.ssl_compression is False:
- self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
+ return self._request('PATCH', url, **kwargs)
- if self.insecure is not True:
- self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
- self.verify_callback)
- 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 "%(cacert)s" %(exc)s' %
- dict(cacert=self.cacert, exc=e))
- raise exc.SSLConfigurationError(msg)
- else:
- self.context.set_default_verify_paths()
-
- def connect(self):
- """
- Connect to an SSL port using the OpenSSL library and apply
- per-connection parameters.
- """
- 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('fL', self.timeout, 0))
- self.sock = OpenSSLConnectionDelegator(self.context, sock)
- self.sock.connect((self.host, self.port))
-
- def close(self):
- if self.sock:
- # Removing reference to socket but don't close it yet.
- # Response close will close both socket and associated
- # file. Closing socket too soon will cause response
- # reads to fail with socket IO error 'Bad file descriptor'.
- self.sock = None
-
- # Calling close on HTTPConnection to continue doing that cleanup.
- HTTPSConnection.close(self)
-
-
-class ResponseBodyIterator(object):
- """
- A class that acts as an iterator over an HTTP response.
-
- This class will also check response body integrity when iterating over
- the instance and if a checksum was supplied using `set_checksum` method,
- else by default the class will not do any integrity check.
- """
-
- def __init__(self, resp):
- self._resp = resp
- self._checksum = None
- self._size = int(resp.getheader('content-length', 0))
- self._end_reached = False
-
- def set_checksum(self, checksum):
- """
- Set checksum to check against when iterating over this instance.
-
- :raise: AttributeError if iterator is already consumed.
- """
- if self._end_reached:
- raise AttributeError("Can't set checksum for an already consumed"
- " iterator")
- self._checksum = checksum
-
- def __len__(self):
- return int(self._size)
-
- def __iter__(self):
- md5sum = hashlib.md5()
- while True:
- try:
- chunk = self.next()
- except StopIteration:
- self._end_reached = True
- # NOTE(mouad): Check image integrity when the end of response
- # body is reached.
- md5sum = md5sum.hexdigest()
- if self._checksum is not None and md5sum != self._checksum:
- raise IOError(errno.EPIPE,
- 'Corrupted image. Checksum was %s '
- 'expected %s' % (md5sum, self._checksum))
- raise
- else:
- yield chunk
- if isinstance(chunk, six.string_types):
- chunk = six.b(chunk)
- md5sum.update(chunk)
-
- def next(self):
- chunk = self._resp.read(CHUNKSIZE)
- if chunk:
- return chunk
- else:
- raise StopIteration()
+ def delete(self, url, **kwargs):
+ return self._request('DELETE', url, **kwargs)
diff --git a/glanceclient/common/https.py b/glanceclient/common/https.py
new file mode 100644
index 0000000..6416c19
--- /dev/null
+++ b/glanceclient/common/https.py
@@ -0,0 +1,274 @@
+# 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 struct
+
+import OpenSSL
+from requests import adapters
+try:
+ from requests.packages.urllib3 import connectionpool
+ from requests.packages.urllib3 import poolmanager
+except ImportError:
+ from urllib3 import connectionpool
+ from urllib3 import poolmanager
+
+import six
+import ssl
+
+from glanceclient.common import utils
+
+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
+ from eventlet.greenio import GreenSocket
+ # TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
+ GreenSocket.getsockopt = utils.getsockopt
+ else:
+ raise ImportError
+except ImportError:
+ try:
+ from httplib import HTTPSConnection
+ except ImportError:
+ from http.client import HTTPSConnection
+ from OpenSSL.SSL import Connection as Connection
+
+
+from glanceclient import exc
+
+
+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 glanceclient's
+ one.
+ """
+
+ def __init__(self, *args, **kwargs):
+ # NOTE(flaper87): This line forces poolmanager to use
+ # glanceclient HTTPSConnection
+ poolmanager.pool_classes_by_scheme["https"] = HTTPSConnectionPool
+ super(HTTPSAdapter, self).__init__(*args, **kwargs)
+
+ def cert_verify(self, conn, url, verify, cert):
+ super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
+ conn.insecure = not verify
+
+
+class HTTPSConnectionPool(connectionpool.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 glanceclient'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 = '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 HTTPSConnection which uses the OpenSSL library
+ for enhanced SSL support.
+ Note: Much of this functionality can eventually be replaced
+ with native Python 3.3 code.
+ """
+ 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:
+ excp_lst = ()
+ 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
+ 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))
+
+ @staticmethod
+ def host_matches_cert(host, x509):
+ """
+ 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 verify_callback(self, connection, x509, errnum,
+ depth, preverify_ok):
+ 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 self.host_matches_cert(self.host, x509)
+ else:
+ # Pass through OpenSSL's default result
+ return preverify_ok
+
+ def set_context(self):
+ """
+ Set up the OpenSSL context.
+ """
+ self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+
+ 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,
+ self.verify_callback)
+ 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 and apply
+ per-connection parameters.
+ """
+ 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))
diff --git a/glanceclient/common/utils.py b/glanceclient/common/utils.py
index 6cdc364..04f5add 100644
--- a/glanceclient/common/utils.py
+++ b/glanceclient/common/utils.py
@@ -16,6 +16,7 @@
from __future__ import print_function
import errno
+import hashlib
import os
import re
import sys
@@ -335,3 +336,22 @@ def print_image(image_obj, max_col_width=None):
print_dict(image, max_column_width=max_col_width)
else:
print_dict(image)
+
+
+def integrity_iter(iter, checksum):
+ """
+ Check image data integrity.
+
+ :raises: IOError
+ """
+ md5sum = hashlib.md5()
+ for chunk in iter:
+ yield chunk
+ if isinstance(chunk, six.string_types):
+ chunk = six.b(chunk)
+ md5sum.update(chunk)
+ md5sum = md5sum.hexdigest()
+ if md5sum != checksum:
+ raise IOError(errno.EPIPE,
+ 'Corrupt image download. Checksum was %s expected %s' %
+ (md5sum, checksum))
diff --git a/glanceclient/exc.py b/glanceclient/exc.py
index 9caa24d..3eeaffa 100644
--- a/glanceclient/exc.py
+++ b/glanceclient/exc.py
@@ -152,7 +152,7 @@ for obj_name in dir(sys.modules[__name__]):
def from_response(response, body=None):
"""Return an instance of an HTTPException based on httplib response."""
- cls = _code_map.get(response.status, HTTPException)
+ cls = _code_map.get(response.status_code, HTTPException)
if body:
details = body.replace('\n\n', '\n')
return cls(details=details)
diff --git a/glanceclient/v1/client.py b/glanceclient/v1/client.py
index 23bb737..aeb94a2 100644
--- a/glanceclient/v1/client.py
+++ b/glanceclient/v1/client.py
@@ -13,10 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from glanceclient.common import http
+from glanceclient.common.http import HTTPClient
from glanceclient.common import utils
-from glanceclient.v1 import image_members
-from glanceclient.v1 import images
+from glanceclient.v1.image_members import ImageMemberManager
+from glanceclient.v1.images import ImageManager
class Client(object):
@@ -31,7 +31,7 @@ class Client(object):
def __init__(self, endpoint, *args, **kwargs):
"""Initialize a new client for the Images v1 API."""
- self.http_client = http.HTTPClient(utils.strip_version(endpoint),
- *args, **kwargs)
- self.images = images.ImageManager(self.http_client)
- self.image_members = image_members.ImageMemberManager(self.http_client)
+ self.http_client = HTTPClient(utils.strip_version(endpoint),
+ *args, **kwargs)
+ self.images = ImageManager(self.http_client)
+ self.image_members = ImageMemberManager(self.http_client)
diff --git a/glanceclient/v1/image_members.py b/glanceclient/v1/image_members.py
index f464fb8..d940a5f 100644
--- a/glanceclient/v1/image_members.py
+++ b/glanceclient/v1/image_members.py
@@ -34,7 +34,7 @@ class ImageMemberManager(base.ManagerWithFind):
def get(self, image, member_id):
image_id = base.getid(image)
url = '/v1/images/%s/members/%s' % (image_id, member_id)
- resp, body = self.client.json_request('GET', url)
+ resp, body = self.client.get(url)
member = body['member']
member['image_id'] = image_id
return ImageMember(self, member, loaded=True)
@@ -60,7 +60,7 @@ class ImageMemberManager(base.ManagerWithFind):
def _list_by_image(self, image):
image_id = base.getid(image)
url = '/v1/images/%s/members' % image_id
- resp, body = self.client.json_request('GET', url)
+ resp, body = self.client.get(url)
out = []
for member in body['members']:
member['image_id'] = image_id
@@ -70,7 +70,7 @@ class ImageMemberManager(base.ManagerWithFind):
def _list_by_member(self, member):
member_id = base.getid(member)
url = '/v1/shared-images/%s' % member_id
- resp, body = self.client.json_request('GET', url)
+ resp, body = self.client.get(url)
out = []
for member in body['shared_images']:
member['member_id'] = member_id
@@ -84,7 +84,7 @@ class ImageMemberManager(base.ManagerWithFind):
"""Creates an image."""
url = '/v1/images/%s/members/%s' % (base.getid(image), member_id)
body = {'member': {'can_share': can_share}}
- self._put(url, json=body)
+ self.client.put(url, data=body)
def replace(self, image, members):
memberships = []
@@ -100,4 +100,4 @@ class ImageMemberManager(base.ManagerWithFind):
obj['can_share'] = member['can_share']
memberships.append(obj)
url = '/v1/images/%s/members' % base.getid(image)
- self.client.json_request('PUT', url, {}, {'memberships': memberships})
+ self.client.put(url, data={'memberships': memberships})
diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py
index d2af595..87060c2 100644
--- a/glanceclient/v1/images.py
+++ b/glanceclient/v1/images.py
@@ -14,10 +14,9 @@
# under the License.
import copy
-import json
import six
-from six.moves.urllib import parse
+import six.moves.urllib.parse as urlparse
from glanceclient.common import utils
from glanceclient.openstack.common.apiclient import base
@@ -60,12 +59,12 @@ class ImageManager(base.ManagerWithFind):
resource_class = Image
def _list(self, url, response_key, obj_class=None, body=None):
- resp = self.client.get(url)
+ resp, body = self.client.get(url)
if obj_class is None:
obj_class = self.resource_class
- data = resp.json()[response_key]
+ data = body[response_key]
return ([obj_class(self, res, loaded=True) for res in data if res],
resp)
@@ -123,13 +122,12 @@ class ImageManager(base.ManagerWithFind):
:rtype: :class:`Image`
"""
image_id = base.getid(image)
- resp, body = self.client.raw_request(
- 'HEAD', '/v1/images/%s' % parse.quote(str(image_id)))
- meta = self._image_meta_from_headers(dict(resp.getheaders()))
+ resp, body = self.client.head('/v1/images/%s'
+ % urlparse.quote(str(image_id)))
+ meta = self._image_meta_from_headers(resp.headers)
return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not None:
- return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None))
-
+ return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
return Image(self, meta)
def data(self, image, do_checksum=True, **kwargs):
@@ -140,14 +138,14 @@ class ImageManager(base.ManagerWithFind):
:rtype: iterable containing image data
"""
image_id = base.getid(image)
- resp, body = self.client.raw_request(
- 'GET', '/v1/images/%s' % parse.quote(str(image_id)))
- checksum = resp.getheader('x-image-meta-checksum', None)
+ resp, body = self.client.get('/v1/images/%s'
+ % urlparse.quote(str(image_id)))
+ checksum = resp.headers.get('x-image-meta-checksum', None)
if do_checksum and checksum is not None:
- body.set_checksum(checksum)
+ return utils.integrity_iter(body, checksum)
return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not None:
- return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None))
+ return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
return body
@@ -194,11 +192,11 @@ class ImageManager(base.ManagerWithFind):
# trying to encode them
qp[param] = strutils.safe_encode(value)
- url = '/v1/images/detail?%s' % parse.urlencode(qp)
+ url = '/v1/images/detail?%s' % urlparse.urlencode(qp)
images, resp = self._list(url, "images")
if return_request_id is not None:
- return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None))
+ return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
for image in images:
if filter_owner(owner, image):
@@ -253,10 +251,11 @@ class ImageManager(base.ManagerWithFind):
def delete(self, image, **kwargs):
"""Delete an image."""
- resp = self._delete("/v1/images/%s" % base.getid(image))[0]
+ url = "/v1/images/%s" % base.getid(image)
+ resp, body = self.client.delete(url)
return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not None:
- return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None))
+ return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
def create(self, **kwargs):
"""Create an image
@@ -284,12 +283,12 @@ class ImageManager(base.ManagerWithFind):
if copy_from is not None:
hdrs['x-glance-api-copy-from'] = copy_from
- resp, body_iter = self.client.raw_request(
- 'POST', '/v1/images', headers=hdrs, body=image_data)
- body = json.loads(''.join([c for c in body_iter]))
+ resp, body = self.client.post('/v1/images',
+ headers=hdrs,
+ data=image_data)
return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not None:
- return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None))
+ return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
return Image(self, self._format_image_meta_for_user(body['image']))
@@ -327,11 +326,9 @@ class ImageManager(base.ManagerWithFind):
hdrs['x-glance-api-copy-from'] = copy_from
url = '/v1/images/%s' % base.getid(image)
- resp, body_iter = self.client.raw_request(
- 'PUT', url, headers=hdrs, body=image_data)
- body = json.loads(''.join([c for c in body_iter]))
+ resp, body = self.client.put(url, headers=hdrs, data=image_data)
return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not None:
- return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None))
+ return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
return Image(self, self._format_image_meta_for_user(body['image']))
diff --git a/glanceclient/v2/image_members.py b/glanceclient/v2/image_members.py
index b2632a2..dcf4ac2 100644
--- a/glanceclient/v2/image_members.py
+++ b/glanceclient/v2/image_members.py
@@ -21,25 +21,22 @@ class Controller(object):
def list(self, image_id):
url = '/v2/images/%s/members' % image_id
- resp, body = self.http_client.json_request('GET', url)
+ resp, body = self.http_client.get(url)
for member in body['members']:
yield self.model(member)
def delete(self, image_id, member_id):
- self.http_client.json_request('DELETE',
- '/v2/images/%s/members/%s' %
- (image_id, member_id))
+ self.http_client.delete('/v2/images/%s/members/%s' %
+ (image_id, member_id))
def update(self, image_id, member_id, member_status):
url = '/v2/images/%s/members/%s' % (image_id, member_id)
body = {'status': member_status}
- resp, updated_member = self.http_client.json_request('PUT', url,
- body=body)
+ resp, updated_member = self.http_client.put(url, data=body)
return self.model(updated_member)
def create(self, image_id, member_id):
url = '/v2/images/%s/members' % image_id
body = {'member': member_id}
- resp, created_member = self.http_client.json_request('POST', url,
- body=body)
+ resp, created_member = self.http_client.post(url, data=body)
return self.model(created_member)
diff --git a/glanceclient/v2/image_tags.py b/glanceclient/v2/image_tags.py
index a943d6a..5c03648 100644
--- a/glanceclient/v2/image_tags.py
+++ b/glanceclient/v2/image_tags.py
@@ -27,7 +27,7 @@ class Controller(object):
:param tag_value: value of the tag.
"""
url = '/v2/images/%s/tags/%s' % (image_id, tag_value)
- self.http_client.json_request('PUT', url)
+ self.http_client.put(url)
def delete(self, image_id, tag_value):
"""
@@ -37,4 +37,4 @@ class Controller(object):
:param tag_value: tag value to be deleted.
"""
url = '/v2/images/%s/tags/%s' % (image_id, tag_value)
- self.http_client.json_request('DELETE', url)
+ self.http_client.delete(url)
diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py
index 10b7864..1144e6f 100644
--- a/glanceclient/v2/images.py
+++ b/glanceclient/v2/images.py
@@ -16,7 +16,6 @@
import json
import six
from six.moves.urllib import parse
-
import warlock
from glanceclient.common import utils
@@ -42,7 +41,7 @@ class Controller(object):
empty_fun = lambda *args, **kwargs: None
def paginate(url):
- resp, body = self.http_client.json_request('GET', url)
+ resp, body = self.http_client.get(url)
for image in body['images']:
# NOTE(bcwaldon): remove 'self' for now until we have
# an elegant way to pass it into the model constructor
@@ -94,7 +93,7 @@ class Controller(object):
def get(self, image_id):
url = '/v2/images/%s' % image_id
- resp, body = self.http_client.json_request('GET', url)
+ resp, body = self.http_client.get(url)
#NOTE(bcwaldon): remove 'self' for now until we have an elegant
# way to pass it into the model constructor without conflict
body.pop('self', None)
@@ -108,11 +107,12 @@ class Controller(object):
:param do_checksum: Enable/disable checksum validation.
"""
url = '/v2/images/%s/file' % image_id
- resp, body = self.http_client.raw_request('GET', url)
- checksum = resp.getheader('content-md5', None)
+ resp, body = self.http_client.get(url)
+ checksum = resp.headers.get('content-md5', None)
if do_checksum and checksum is not None:
- body.set_checksum(checksum)
- return body
+ return utils.integrity_iter(body, checksum)
+ else:
+ return body
def upload(self, image_id, image_data, image_size=None):
"""
@@ -124,14 +124,17 @@ class Controller(object):
"""
url = '/v2/images/%s/file' % image_id
hdrs = {'Content-Type': 'application/octet-stream'}
- self.http_client.raw_request('PUT', url,
- headers=hdrs,
- body=image_data,
- content_length=image_size)
+ if image_size:
+ body = {'image_data': image_data,
+ 'image_size': image_size}
+ else:
+ body = image_data
+ self.http_client.put(url, headers=hdrs, data=body)
def delete(self, image_id):
"""Delete an image."""
- self.http_client.json_request('DELETE', '/v2/images/%s' % image_id)
+ url = '/v2/images/%s' % image_id
+ self.http_client.delete(url)
def create(self, **kwargs):
"""Create an image."""
@@ -144,7 +147,7 @@ class Controller(object):
except warlock.InvalidOperation as e:
raise TypeError(utils.exception_to_str(e))
- resp, body = self.http_client.json_request('POST', url, body=image)
+ resp, body = self.http_client.post(url, data=image)
#NOTE(esheffield): remove 'self' for now until we have an elegant
# way to pass it into the model constructor without conflict
body.pop('self', None)
@@ -178,9 +181,7 @@ class Controller(object):
url = '/v2/images/%s' % image_id
hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
- self.http_client.raw_request('PATCH', url,
- headers=hdrs,
- body=image.patch)
+ self.http_client.patch(url, headers=hdrs, data=image.patch)
#NOTE(bcwaldon): calling image.patch doesn't clear the changes, so
# we need to fetch the image again to get a clean history. This is
@@ -197,9 +198,7 @@ class Controller(object):
def _send_image_update_request(self, image_id, patch_body):
url = '/v2/images/%s' % image_id
hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
- self.http_client.raw_request('PATCH', url,
- headers=hdrs,
- body=json.dumps(patch_body))
+ self.http_client.patch(url, headers=hdrs, data=json.dumps(patch_body))
def add_location(self, image_id, url, metadata):
"""Add a new location entry to an image's list of locations.
diff --git a/glanceclient/v2/schemas.py b/glanceclient/v2/schemas.py
index 57f6cc7..7cd169d 100644
--- a/glanceclient/v2/schemas.py
+++ b/glanceclient/v2/schemas.py
@@ -81,5 +81,5 @@ class Controller(object):
def get(self, schema_name):
uri = '/v2/schemas/%s' % schema_name
- _, raw_schema = self.http_client.json_request('GET', uri)
+ _, raw_schema = self.http_client.get(uri)
return Schema(raw_schema)
diff --git a/requirements.txt b/requirements.txt
index b2d752f..dfae8b7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,5 +4,6 @@ argparse
PrettyTable>=0.7,<0.8
python-keystoneclient>=0.9.0
pyOpenSSL>=0.11
+requests>=1.1
warlock>=1.0.1,<2
six>=1.7.0
diff --git a/tests/test_exc.py b/tests/test_exc.py
index d5105bc..c8ad2df 100644
--- a/tests/test_exc.py
+++ b/tests/test_exc.py
@@ -12,18 +12,16 @@
# 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 collections
+import mock
import testtools
from glanceclient import exc
-FakeResponse = collections.namedtuple('HTTPResponse', ['status'])
-
-
class TestHTTPExceptions(testtools.TestCase):
def test_from_response(self):
"""exc.from_response should return instance of an HTTP exception."""
- out = exc.from_response(FakeResponse(400))
+ mock_resp = mock.Mock()
+ mock_resp.status_code = 400
+ out = exc.from_response(mock_resp)
self.assertIsInstance(out, exc.HTTPBadRequest)
diff --git a/tests/test_http.py b/tests/test_http.py
index 8dabdba..0c8a4ac 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -12,21 +12,18 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+import json
-import errno
-import socket
-
-import mock
from mox3 import mox
+import requests
import six
-from six.moves import http_client
from six.moves.urllib import parse
-import tempfile
import testtools
+import types
import glanceclient
from glanceclient.common import http
-from glanceclient.common import utils as client_utils
+from glanceclient.common import https
from glanceclient import exc
from tests import utils
@@ -36,8 +33,7 @@ class TestClient(testtools.TestCase):
def setUp(self):
super(TestClient, self).setUp()
self.mock = mox.Mox()
- self.mock.StubOutWithMock(http_client.HTTPConnection, 'request')
- self.mock.StubOutWithMock(http_client.HTTPConnection, 'getresponse')
+ self.mock.StubOutWithMock(requests.Session, 'request')
self.endpoint = 'http://example.com:9292'
self.client = http.HTTPClient(self.endpoint, token=u'abc123')
@@ -85,14 +81,16 @@ class TestClient(testtools.TestCase):
And the error should list the host and port that refused the
connection
"""
- http_client.HTTPConnection.request(
+ requests.Session.request(
mox.IgnoreArg(),
mox.IgnoreArg(),
+ data=mox.IgnoreArg(),
headers=mox.IgnoreArg(),
- ).AndRaise(socket.error())
+ stream=mox.IgnoreArg(),
+ ).AndRaise(requests.exceptions.ConnectionError())
self.mock.ReplayAll()
try:
- self.client.json_request('GET', '/v1/images/detail?limit=20')
+ self.client.get('/v1/images/detail?limit=20')
#NOTE(alaski) We expect exc.CommunicationError to be raised
# so we should never reach this point. try/except is used here
# rather than assertRaises() so that we can check the body of
@@ -103,47 +101,23 @@ class TestClient(testtools.TestCase):
(comm_err.message, self.endpoint))
self.assertTrue(self.endpoint in comm_err.message, fail_msg)
- def test_request_redirected(self):
- resp = utils.FakeResponse({'location': 'http://www.example.com'},
- status=302, body=six.BytesIO())
- http_client.HTTPConnection.request(
- mox.IgnoreArg(),
- mox.IgnoreArg(),
- headers=mox.IgnoreArg(),
- )
- http_client.HTTPConnection.getresponse().AndReturn(resp)
-
- # The second request should be to the redirected location
- expected_response = b'Ok'
- resp2 = utils.FakeResponse({}, six.BytesIO(expected_response))
- http_client.HTTPConnection.request(
- 'GET',
- 'http://www.example.com',
- headers=mox.IgnoreArg(),
- )
- http_client.HTTPConnection.getresponse().AndReturn(resp2)
-
- self.mock.ReplayAll()
-
- self.client.json_request('GET', '/v1/images/detail')
-
def test_http_encoding(self):
- http_client.HTTPConnection.request(
+ # Lets fake the response
+ # returned by requests
+ response = 'Ok'
+ headers = {"Content-Type": "text/plain"}
+ fake = utils.FakeResponse(headers, six.StringIO(response))
+ requests.Session.request(
mox.IgnoreArg(),
mox.IgnoreArg(),
- headers=mox.IgnoreArg())
-
- # Lets fake the response
- # returned by httplib
- expected_response = b'Ok'
- fake = utils.FakeResponse({}, six.BytesIO(expected_response))
- http_client.HTTPConnection.getresponse().AndReturn(fake)
+ data=mox.IgnoreArg(),
+ stream=mox.IgnoreArg(),
+ headers=mox.IgnoreArg()).AndReturn(fake)
self.mock.ReplayAll()
headers = {"test": u'ni\xf1o'}
- resp, body = self.client.raw_request('GET', '/v1/images/detail',
- headers=headers)
- self.assertEqual(fake, resp)
+ resp, body = self.client.get('/v1/images/detail', headers=headers)
+ self.assertEqual(resp, fake)
def test_headers_encoding(self):
value = u'ni\xf1o'
@@ -156,153 +130,19 @@ class TestClient(testtools.TestCase):
def test_raw_request(self):
" Verify the path being used for HTTP requests reflects accurately. "
-
- def check_request(method, path, **kwargs):
- self.assertEqual('GET', method)
- # NOTE(kmcdonald): See bug #1179984 for more details.
- self.assertEqual('/v1/images/detail', path)
-
- http_client.HTTPConnection.request(
+ headers = {"Content-Type": "text/plain"}
+ response = 'Ok'
+ fake = utils.FakeResponse({}, six.StringIO(response))
+ requests.Session.request(
mox.IgnoreArg(),
mox.IgnoreArg(),
- headers=mox.IgnoreArg()).WithSideEffects(check_request)
-
- # fake the response returned by httplib
- fake = utils.FakeResponse({}, six.BytesIO(b'Ok'))
- http_client.HTTPConnection.getresponse().AndReturn(fake)
+ data=mox.IgnoreArg(),
+ stream=mox.IgnoreArg(),
+ headers=mox.IgnoreArg()).AndReturn(fake)
self.mock.ReplayAll()
- resp, body = self.client.raw_request('GET', '/v1/images/detail')
- self.assertEqual(fake, resp)
-
- def test_customized_path_raw_request(self):
- """
- Verify the customized path being used for HTTP requests
- reflects accurately
- """
-
- def check_request(method, path, **kwargs):
- self.assertEqual('GET', method)
- self.assertEqual('/customized-path/v1/images/detail', path)
-
- # NOTE(yuyangbj): see bug 1230032 to get more info
- endpoint = 'http://example.com:9292/customized-path'
- client = http.HTTPClient(endpoint, token=u'abc123')
- self.assertEqual('/customized-path', client.endpoint_path)
-
- http_client.HTTPConnection.request(
- mox.IgnoreArg(),
- mox.IgnoreArg(),
- headers=mox.IgnoreArg()).WithSideEffects(check_request)
-
- # fake the response returned by httplib
- fake = utils.FakeResponse({}, six.BytesIO(b'Ok'))
- http_client.HTTPConnection.getresponse().AndReturn(fake)
- self.mock.ReplayAll()
-
- resp, body = client.raw_request('GET', '/v1/images/detail')
- self.assertEqual(fake, resp)
-
- def test_raw_request_no_content_length(self):
- with tempfile.NamedTemporaryFile() as test_file:
- test_file.write(b'abcd')
- test_file.seek(0)
- data_length = 4
- self.assertEqual(data_length,
- client_utils.get_file_size(test_file))
-
- exp_resp = {'body': test_file}
- exp_resp['headers'] = {'Content-Length': str(data_length),
- 'Content-Type': 'application/octet-stream'}
-
- def mock_request(url, method, **kwargs):
- return kwargs
-
- rq_kwargs = {'body': test_file, 'content_length': None}
-
- with mock.patch.object(self.client, '_http_request') as mock_rq:
- mock_rq.side_effect = mock_request
- resp = self.client.raw_request('PUT', '/v1/images/detail',
- **rq_kwargs)
-
- rq_kwargs.pop('content_length')
- headers = {'Content-Length': str(data_length),
- 'Content-Type': 'application/octet-stream'}
- rq_kwargs['headers'] = headers
-
- mock_rq.assert_called_once_with('/v1/images/detail', 'PUT',
- **rq_kwargs)
-
- self.assertEqual(exp_resp, resp)
-
- def test_raw_request_w_content_length(self):
- with tempfile.NamedTemporaryFile() as test_file:
- test_file.write(b'abcd')
- test_file.seek(0)
- data_length = 4
- self.assertEqual(data_length,
- client_utils.get_file_size(test_file))
-
- exp_resp = {'body': test_file}
- # NOTE: we expect the actual file size to be overridden by the
- # supplied content length.
- exp_resp['headers'] = {'Content-Length': '4',
- 'Content-Type': 'application/octet-stream'}
-
- def mock_request(url, method, **kwargs):
- return kwargs
-
- rq_kwargs = {'body': test_file, 'content_length': data_length}
-
- with mock.patch.object(self.client, '_http_request') as mock_rq:
- mock_rq.side_effect = mock_request
- resp = self.client.raw_request('PUT', '/v1/images/detail',
- **rq_kwargs)
-
- rq_kwargs.pop('content_length')
- headers = {'Content-Length': str(data_length),
- 'Content-Type': 'application/octet-stream'}
- rq_kwargs['headers'] = headers
-
- mock_rq.assert_called_once_with('/v1/images/detail', 'PUT',
- **rq_kwargs)
-
- self.assertEqual(exp_resp, resp)
-
- def test_raw_request_w_bad_content_length(self):
- with tempfile.NamedTemporaryFile() as test_file:
- test_file.write(b'abcd')
- test_file.seek(0)
- self.assertEqual(4, client_utils.get_file_size(test_file))
-
- def mock_request(url, method, **kwargs):
- return kwargs
-
- with mock.patch.object(self.client, '_http_request', mock_request):
- self.assertRaises(AttributeError, self.client.raw_request,
- 'PUT', '/v1/images/detail', body=test_file,
- content_length=32)
-
- def test_connection_refused_raw_request(self):
- """
- Should receive a CommunicationError if connection refused.
- And the error should list the host and port that refused the
- connection
- """
- endpoint = 'http://example.com:9292'
- client = http.HTTPClient(endpoint, token=u'abc123')
- http_client.HTTPConnection.request(mox.IgnoreArg(), mox.IgnoreArg(),
- headers=mox.IgnoreArg()
- ).AndRaise(socket.error())
- self.mock.ReplayAll()
- try:
- client.raw_request('GET', '/v1/images/detail?limit=20')
-
- self.fail('An exception should have bypassed this line.')
- except exc.CommunicationError as comm_err:
- fail_msg = ("Exception message '%s' should contain '%s'" %
- (comm_err.message, endpoint))
- self.assertTrue(endpoint in comm_err.message, fail_msg)
+ resp, body = self.client.get('/v1/images/detail', headers=headers)
+ self.assertEqual(resp, fake)
def test_parse_endpoint(self):
endpoint = 'http://example.com:9292'
@@ -313,81 +153,84 @@ class TestClient(testtools.TestCase):
query='', fragment='')
self.assertEqual(expected, actual)
- def test_get_connection_class(self):
- endpoint = 'http://example.com:9292'
- test_client = http.HTTPClient(endpoint, token=u'adc123')
- actual = (test_client.get_connection_class('https'))
- self.assertEqual(http.VerifiedHTTPSConnection, actual)
-
def test_get_connections_kwargs_http(self):
endpoint = 'http://example.com:9292'
test_client = http.HTTPClient(endpoint, token=u'adc123')
- actual = test_client.get_connection_kwargs('http', insecure=True)
- self.assertEqual({'timeout': 600.0}, actual)
-
- def test_get_connections_kwargs_https(self):
- endpoint = 'http://example.com:9292'
- test_client = http.HTTPClient(endpoint, token=u'adc123')
- actual = test_client.get_connection_kwargs('https', insecure=True)
- expected = {'cacert': None,
- 'cert_file': None,
- 'insecure': True,
- 'key_file': None,
- 'ssl_compression': True,
- 'timeout': 600.0}
- self.assertEqual(expected, actual)
-
- def test_log_curl_request_with_non_ascii_char(self):
- try:
- headers = {'header1': 'value1\xa5\xa6'}
- http_client_object = http.HTTPClient(self.endpoint)
- http_client_object.log_curl_request('GET',
- 'http://www.example.com/\xa5',
- {'headers': headers})
- except UnicodeDecodeError as e:
- self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
+ self.assertEqual(test_client.timeout, 600.0)
+ def test_http_chunked_request(self):
+ # Lets fake the response
+ # returned by requests
+ response = "Ok"
+ data = six.StringIO(response)
+ fake = utils.FakeResponse({}, data)
+ requests.Session.request(
+ mox.IgnoreArg(),
+ mox.IgnoreArg(),
+ stream=mox.IgnoreArg(),
+ data=mox.IsA(types.GeneratorType),
+ headers=mox.IgnoreArg()).AndReturn(fake)
+ self.mock.ReplayAll()
-class TestHostResolutionError(testtools.TestCase):
+ headers = {"test": u'chunked_request'}
+ resp, body = self.client.post('/v1/images/',
+ headers=headers, data=data)
+ self.assertEqual(resp, fake)
- def setUp(self):
- super(TestHostResolutionError, self).setUp()
- self.mock = mox.Mox()
- self.invalid_host = "example.com.incorrect_top_level_domain"
+ def test_http_json(self):
+ data = {"test": "json_request"}
+ fake = utils.FakeResponse({}, "OK")
- def test_incorrect_domain_error(self):
- """
- Make sure that using a domain which does not resolve causes an
- exception which mentions that specific hostname as a reason for
- failure.
- """
- class FailingConnectionClass(object):
- def __init__(self, *args, **kwargs):
- pass
+ def test_json(passed_data):
+ """
+ This function tests whether the data
+ being passed to request's method is
+ a valid json or not.
- def putrequest(self, *args, **kwargs):
- raise socket.gaierror(-2, "Name or service not known")
+ This function will be called by pymox
- def request(self, *args, **kwargs):
- raise socket.gaierror(-2, "Name or service not known")
+ :params passed_data: The data being
+ passed to requests.Session.request.
+ """
+ if not isinstance(passed_data, six.string_types):
+ return False
- self.endpoint = 'http://%s:9292' % (self.invalid_host,)
- self.client = http.HTTPClient(self.endpoint, token=u'abc123')
+ try:
+ passed_data = json.loads(passed_data)
+ return data == passed_data
+ except (TypeError, ValueError):
+ return False
- self.mock.StubOutWithMock(self.client, 'get_connection')
- self.client.get_connection().AndReturn(FailingConnectionClass())
+ requests.Session.request(
+ mox.IgnoreArg(),
+ mox.IgnoreArg(),
+ stream=mox.IgnoreArg(),
+ data=mox.Func(test_json),
+ headers=mox.IgnoreArg()).AndReturn(fake)
self.mock.ReplayAll()
- try:
- self.client.raw_request('GET', '/example/path')
- self.fail("gaierror should be raised")
- except exc.InvalidEndpoint as e:
- self.assertTrue(self.invalid_host in str(e),
- "exception should contain the hostname")
+ headers = {"test": u'chunked_request'}
+ resp, body = self.client.post('/v1/images/',
+ headers=headers,
+ data=data)
+ self.assertEqual(resp, fake)
- def tearDown(self):
- super(TestHostResolutionError, self).tearDown()
- self.mock.UnsetStubs()
+ def test_http_chunked_response(self):
+ headers = {"Content-Type": "application/octet-stream"}
+ data = "TEST"
+ fake = utils.FakeResponse(headers, six.StringIO(data))
+
+ requests.Session.request(
+ mox.IgnoreArg(),
+ mox.IgnoreArg(),
+ stream=mox.IgnoreArg(),
+ data=mox.IgnoreArg(),
+ headers=mox.IgnoreArg()).AndReturn(fake)
+ self.mock.ReplayAll()
+ headers = {"test": u'chunked_request'}
+ resp, body = self.client.get('/v1/images/')
+ self.assertTrue(isinstance(body, types.GeneratorType))
+ self.assertEqual([data], list(body))
class TestVerifiedHTTPSConnection(testtools.TestCase):
@@ -396,7 +239,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
def test_setcontext_unable_to_load_cacert(self):
"""Add this UT case with Bug#1265730."""
self.assertRaises(exc.SSLConfigurationError,
- http.VerifiedHTTPSConnection,
+ https.VerifiedHTTPSConnection,
"127.0.0.1",
None,
None,
@@ -405,45 +248,3 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
None,
False,
True)
-
-
-class TestResponseBodyIterator(testtools.TestCase):
-
- def test_iter_default_chunk_size_64k(self):
- resp = utils.FakeResponse({}, six.BytesIO(b'X' * 98304))
- iterator = http.ResponseBodyIterator(resp)
- chunks = list(iterator)
- self.assertEqual([b'X' * 65536, b'X' * 32768], chunks)
-
- def test_integrity_check_with_correct_checksum(self):
- resp = utils.FakeResponse({}, six.BytesIO(b'CCC'))
- body = http.ResponseBodyIterator(resp)
- body.set_checksum('defb99e69a9f1f6e06f15006b1f166ae')
- list(body)
-
- def test_integrity_check_with_wrong_checksum(self):
- resp = utils.FakeResponse({}, six.BytesIO(b'BB'))
- body = http.ResponseBodyIterator(resp)
- body.set_checksum('wrong')
- try:
- list(body)
- self.fail('integrity checked passed with wrong checksum')
- except IOError as e:
- self.assertEqual(errno.EPIPE, e.errno)
-
- def test_set_checksum_in_consumed_iterator(self):
- resp = utils.FakeResponse({}, six.BytesIO(b'CCC'))
- body = http.ResponseBodyIterator(resp)
- list(body)
- # Setting checksum for an already consumed iterator should raise an
- # AttributeError.
- self.assertRaises(
- AttributeError, body.set_checksum,
- 'defb99e69a9f1f6e06f15006b1f166ae')
-
- def test_body_size(self):
- size = 1000000007
- resp = utils.FakeResponse(
- {'content-length': str(size)}, six.BytesIO(b'BB'))
- body = http.ResponseBodyIterator(resp)
- self.assertEqual(size, len(body))
diff --git a/tests/test_shell.py b/tests/test_shell.py
index ebe77e0..28f6b13 100644
--- a/tests/test_shell.py
+++ b/tests/test_shell.py
@@ -105,13 +105,11 @@ class ShellCacheSchemaTest(utils.TestCase):
super(ShellCacheSchemaTest, self).setUp()
self._mock_client_setup()
self._mock_shell_setup()
- os.path.exists = mock.MagicMock()
self.cache_dir = '/dir_for_cached_schema'
self.cache_file = self.cache_dir + '/image_schema.json'
def tearDown(self):
super(ShellCacheSchemaTest, self).tearDown()
- os.path.exists.reset_mock()
def _mock_client_setup(self):
self.schema_dict = {
@@ -137,13 +135,10 @@ class ShellCacheSchemaTest(utils.TestCase):
return Args(args)
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
- def test_cache_schema_gets_when_not_exists(self):
- mocked_path_exists_result_lst = [True, False]
- os.path.exists.side_effect = \
- lambda *args: mocked_path_exists_result_lst.pop(0)
-
+ @mock.patch('os.path.exists', return_value=True)
+ def test_cache_schema_gets_when_forced(self, exists_mock):
options = {
- 'get_schema': False
+ 'get_schema': True
}
self.shell._cache_schema(self._make_args(options),
@@ -155,11 +150,10 @@ class ShellCacheSchemaTest(utils.TestCase):
open.mock_calls[2])
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
- def test_cache_schema_gets_when_forced(self):
- os.path.exists.return_value = True
-
+ @mock.patch('os.path.exists', side_effect=[True, False])
+ def test_cache_schema_gets_when_not_exists(self, exists_mock):
options = {
- 'get_schema': True
+ 'get_schema': False
}
self.shell._cache_schema(self._make_args(options),
@@ -171,9 +165,8 @@ class ShellCacheSchemaTest(utils.TestCase):
open.mock_calls[2])
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
- def test_cache_schema_leaves_when_present_not_forced(self):
- os.path.exists.return_value = True
-
+ @mock.patch('os.path.exists', return_value=True)
+ def test_cache_schema_leaves_when_present_not_forced(self, exists_mock):
options = {
'get_schema': False
}
@@ -183,5 +176,5 @@ class ShellCacheSchemaTest(utils.TestCase):
os.path.exists.assert_any_call(self.cache_dir)
os.path.exists.assert_any_call(self.cache_file)
- self.assertEqual(2, os.path.exists.call_count)
+ self.assertEqual(2, exists_mock.call_count)
self.assertEqual(0, open.mock_calls.__len__())
diff --git a/tests/test_ssl.py b/tests/test_ssl.py
index f8278a7..ecbccfa 100644
--- a/tests/test_ssl.py
+++ b/tests/test_ssl.py
@@ -16,9 +16,11 @@
import os
from OpenSSL import crypto
+from requests.packages.urllib3 import poolmanager
import testtools
from glanceclient.common import http
+from glanceclient.common import https
from glanceclient import exc
@@ -26,6 +28,26 @@ TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'var'))
+class TestRequestsIntegration(testtools.TestCase):
+
+ def test_pool_patch(self):
+ client = http.HTTPClient("https://localhost",
+ ssl_compression=True)
+ self.assertNotEqual(https.HTTPSConnectionPool,
+ poolmanager.pool_classes_by_scheme["https"])
+
+ adapter = client.session.adapters.get("https://")
+ self.assertFalse(isinstance(adapter, https.HTTPSAdapter))
+
+ client = http.HTTPClient("https://localhost",
+ ssl_compression=False)
+ self.assertEqual(https.HTTPSConnectionPool,
+ poolmanager.pool_classes_by_scheme["https"])
+
+ adapter = client.session.adapters.get("https://")
+ self.assertTrue(isinstance(adapter, https.HTTPSAdapter))
+
+
class TestVerifiedHTTPSConnection(testtools.TestCase):
def test_ssl_init_ok(self):
"""
@@ -35,10 +57,10 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- key_file=key_file,
- cert_file=cert_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert)
except exc.SSLConfigurationError:
self.fail('Failed to init VerifiedHTTPSConnection.')
@@ -49,9 +71,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- cert_file=cert_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError:
pass
@@ -63,9 +85,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- key_file=key_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cacert=cacert)
except exc.SSLConfigurationError:
pass
except Exception:
@@ -78,9 +100,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- cert_file=cert_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError:
pass
@@ -92,9 +114,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- cert_file=cert_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError:
pass
@@ -106,9 +128,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'badca.crt')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- cert_file=cert_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError:
pass
@@ -123,7 +145,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try:
- conn = http.VerifiedHTTPSConnection('0.0.0.0', 0)
+ conn = https.VerifiedHTTPSConnection('0.0.0.0', 0)
conn.verify_callback(None, cert, 0, 0, 1)
except Exception:
self.fail('Unexpected exception.')
@@ -138,7 +160,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=*.pong.example.com
self.assertEqual('*.pong.example.com', cert.get_subject().commonName)
try:
- conn = http.VerifiedHTTPSConnection('ping.pong.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('ping.pong.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1)
except Exception:
self.fail('Unexpected exception.')
@@ -153,13 +175,13 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try:
- conn = http.VerifiedHTTPSConnection('alt1.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('alt1.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1)
except Exception:
self.fail('Unexpected exception.')
try:
- conn = http.VerifiedHTTPSConnection('alt2.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('alt2.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1)
except Exception:
self.fail('Unexpected exception.')
@@ -174,19 +196,19 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try:
- conn = http.VerifiedHTTPSConnection('alt1.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('alt1.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1)
except Exception:
self.fail('Unexpected exception.')
try:
- conn = http.VerifiedHTTPSConnection('alt2.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('alt2.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1)
except Exception:
self.fail('Unexpected exception.')
try:
- conn = http.VerifiedHTTPSConnection('alt3.example.net', 0)
+ conn = https.VerifiedHTTPSConnection('alt3.example.net', 0)
conn.verify_callback(None, cert, 0, 0, 1)
self.fail('Failed to raise assertion.')
except exc.SSLCertificateError:
@@ -202,7 +224,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try:
- conn = http.VerifiedHTTPSConnection('mismatch.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('mismatch.example.com', 0)
except Exception:
self.fail('Failed to init VerifiedHTTPSConnection.')
@@ -220,10 +242,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
self.assertEqual('openstack.example.com',
cert.get_subject().commonName)
try:
- conn = http.VerifiedHTTPSConnection('openstack.example.com', 0)
+ conn = https.VerifiedHTTPSConnection('openstack.example.com', 0)
except Exception:
self.fail('Failed to init VerifiedHTTPSConnection.')
-
self.assertRaises(exc.SSLCertificateError,
conn.verify_callback, None, cert, 0, 0, 1)
@@ -236,7 +257,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
key_file = 'fake.key'
self.assertRaises(
exc.SSLConfigurationError,
- http.VerifiedHTTPSConnection, '127.0.0.1',
+ https.VerifiedHTTPSConnection, '127.0.0.1',
0, key_file=key_file,
cert_file=cert_file, cacert=cacert)
@@ -248,7 +269,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection(
+ https.VerifiedHTTPSConnection(
'127.0.0.1', 0,
key_file=key_file,
cert_file=cert_file,
@@ -264,7 +285,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
- http.VerifiedHTTPSConnection(
+ https.VerifiedHTTPSConnection(
'127.0.0.1', 0,
key_file=key_file,
cert_file=cert_file,
@@ -286,9 +307,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = cert_file.encode('ascii', 'strict').decode('utf-8')
cacert = cacert.encode('ascii', 'strict').decode('utf-8')
try:
- http.VerifiedHTTPSConnection('127.0.0.1', 0,
- key_file=key_file,
- cert_file=cert_file,
- cacert=cacert)
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert)
except exc.SSLConfigurationError:
self.fail('Failed to init VerifiedHTTPSConnection.')
diff --git a/tests/utils.py b/tests/utils.py
index 2eb5f02..0fa0e2e 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -14,64 +14,53 @@
# under the License.
import copy
-import requests
+import json
import six
import testtools
-from glanceclient.common import http
-
class FakeAPI(object):
def __init__(self, fixtures):
self.fixtures = fixtures
self.calls = []
- def _request(self, method, url, headers=None, body=None,
+ def _request(self, method, url, headers=None, data=None,
content_length=None):
- call = (method, url, headers or {}, body)
+ call = (method, url, headers or {}, data)
if content_length is not None:
call = tuple(list(call) + [content_length])
self.calls.append(call)
- return self.fixtures[url][method]
-
- def raw_request(self, *args, **kwargs):
- fixture = self._request(*args, **kwargs)
- resp = FakeResponse(fixture[0], six.StringIO(fixture[1]))
- body_iter = http.ResponseBodyIterator(resp)
- return resp, body_iter
+ fixture = self.fixtures[url][method]
- def json_request(self, *args, **kwargs):
- fixture = self._request(*args, **kwargs)
- return FakeResponse(fixture[0]), fixture[1]
+ data = fixture[1]
+ if isinstance(fixture[1], six.string_types):
+ try:
+ data = json.loads(fixture[1])
+ except ValueError:
+ data = six.StringIO(fixture[1])
- def client_request(self, method, url, **kwargs):
- if 'json' in kwargs and 'body' not in kwargs:
- kwargs['body'] = kwargs.pop('json')
- resp, body = self.json_request(method, url, **kwargs)
- resp.json = lambda: body
- resp.content = bool(body)
- return resp
+ return FakeResponse(fixture[0], fixture[1]), data
- def head(self, url, **kwargs):
- return self.client_request("HEAD", url, **kwargs)
+ def get(self, *args, **kwargs):
+ return self._request('GET', *args, **kwargs)
- def get(self, url, **kwargs):
- return self.client_request("GET", url, **kwargs)
+ def post(self, *args, **kwargs):
+ return self._request('POST', *args, **kwargs)
- def post(self, url, **kwargs):
- return self.client_request("POST", url, **kwargs)
+ def put(self, *args, **kwargs):
+ return self._request('PUT', *args, **kwargs)
- def put(self, url, **kwargs):
- return self.client_request("PUT", url, **kwargs)
+ def patch(self, *args, **kwargs):
+ return self._request('PATCH', *args, **kwargs)
- def delete(self, url, **kwargs):
- return self.raw_request("DELETE", url, **kwargs)
+ def delete(self, *args, **kwargs):
+ return self._request('DELETE', *args, **kwargs)
- def patch(self, url, **kwargs):
- return self.client_request("PATCH", url, **kwargs)
+ def head(self, *args, **kwargs):
+ return self._request('HEAD', *args, **kwargs)
-class FakeResponse(object):
+class RawRequest(object):
def __init__(self, headers, body=None,
version=1.0, status=200, reason="Ok"):
"""
@@ -97,34 +86,53 @@ class FakeResponse(object):
return self.body.read(amt)
-class TestCase(testtools.TestCase):
- TEST_REQUEST_BASE = {
- 'config': {'danger_mode': False},
- 'verify': True}
+class FakeResponse(object):
+ def __init__(self, headers=None, body=None,
+ version=1.0, status_code=200, reason="Ok"):
+ """
+ :param headers: dict representing HTTP response headers
+ :param body: file-like object
+ :param version: HTTP Version
+ :param status: Response status code
+ :param reason: Status code related message.
+ """
+ self.body = body
+ self.reason = reason
+ self.version = version
+ self.headers = headers
+ self.status_code = status_code
+ self.raw = RawRequest(headers, body=body, reason=reason,
+ version=version, status=status_code)
+ @property
+ def ok(self):
+ return (self.status_code < 400 or
+ self.status_code >= 600)
-class TestResponse(requests.Response):
- """
- Class used to wrap requests.Response and provide some
- convenience to initialize with a dict
- """
- def __init__(self, data):
- self._text = None
- super(TestResponse, self)
- if isinstance(data, dict):
- self.status_code = data.get('status_code', None)
- self.headers = data.get('headers', None)
- # Fake the text attribute to streamline Response creation
- self._text = data.get('text', None)
- else:
- self.status_code = data
-
- def __eq__(self, other):
- return self.__dict__ == other.__dict__
+ def read(self, amt):
+ return self.body.read(amt)
@property
- def text(self):
- return self._text
+ def content(self):
+ if hasattr(self.body, "read"):
+ return self.body.read()
+ return self.body
+
+ def json(self, **kwargs):
+ return self.body and json.loads(self.content) or ""
+
+ def iter_content(self, chunk_size=1, decode_unicode=False):
+ while True:
+ chunk = self.raw.read(chunk_size)
+ if not chunk:
+ break
+ yield chunk
+
+
+class TestCase(testtools.TestCase):
+ TEST_REQUEST_BASE = {
+ 'config': {'danger_mode': False},
+ 'verify': True}
class FakeTTYStdout(six.StringIO):
diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py
index b42cd82..e63634f 100644
--- a/tests/v1/test_images.py
+++ b/tests/v1/test_images.py
@@ -932,7 +932,7 @@ class ParameterFakeAPI(utils.FakeAPI):
},
]}
- def json_request(self, method, url, **kwargs):
+ def get(self, url, **kwargs):
self.url = url
return utils.FakeResponse({}), ParameterFakeAPI.image_list
diff --git a/tests/v1/test_legacy_shell.py b/tests/v1/test_legacy_shell.py
index e86d6e6..2ccb748 100644
--- a/tests/v1/test_legacy_shell.py
+++ b/tests/v1/test_legacy_shell.py
@@ -257,7 +257,7 @@ class LegacyShellV1Test(testtools.TestCase):
args = Image()
gc = client.Client('1', 'http://is.invalid')
self.assertRaises(
- exc.InvalidEndpoint, test_shell.do_update, gc, args)
+ exc.CommunicationError, test_shell.do_update, gc, args)
def test_do_update(self):
class Image():
diff --git a/tests/v1/test_shell.py b/tests/v1/test_shell.py
index d9cccd5..71ee11b 100644
--- a/tests/v1/test_shell.py
+++ b/tests/v1/test_shell.py
@@ -224,81 +224,81 @@ class ShellInvalidEndpointandParameterTest(utils.TestCase):
def test_image_list_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint, self.run_command, 'image-list')
+ exc.CommunicationError, self.run_command, 'image-list')
def test_image_details_invalid_endpoint_legacy(self):
self.assertRaises(
- exc.InvalidEndpoint, self.run_command, 'details')
+ exc.CommunicationError, self.run_command, 'details')
def test_image_update_invalid_endpoint_legacy(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'update {"name":""test}')
def test_image_index_invalid_endpoint_legacy(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'index')
def test_image_create_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'image-create')
def test_image_delete_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'image-delete <fake>')
def test_image_download_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'image-download <fake>')
def test_image_members_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'image-members fake_id')
def test_members_list_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'member-list --image-id fake')
def test_member_replace_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'members-replace image_id member_id')
def test_image_show_invalid_endpoint_legacy(self):
self.assertRaises(
- exc.InvalidEndpoint, self.run_command, 'show image')
+ exc.CommunicationError, self.run_command, 'show image')
def test_image_show_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'image-show --human-readable <IMAGE_ID>')
def test_member_images_invalid_endpoint_legacy(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command, 'member-images member_id')
def test_member_create_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command,
'member-create --can-share <IMAGE_ID> <TENANT_ID>')
def test_member_delete_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command,
'member-delete <IMAGE_ID> <TENANT_ID>')
def test_member_add_invalid_endpoint(self):
self.assertRaises(
- exc.InvalidEndpoint,
+ exc.CommunicationError,
self.run_command,
'member-add <IMAGE_ID> <TENANT_ID>')
diff --git a/tests/v2/test_images.py b/tests/v2/test_images.py
index c93e34e..d231a31 100644
--- a/tests/v2/test_images.py
+++ b/tests/v2/test_images.py
@@ -514,9 +514,11 @@ class TestController(testtools.TestCase):
image_data = 'CCC'
image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
self.controller.upload(image_id, image_data, image_size=3)
+ body = {'image_data': image_data,
+ 'image_size': 3}
expect = [('PUT', '/v2/images/%s/file' % image_id,
{'Content-Type': 'application/octet-stream'},
- image_data, 3)]
+ body)]
self.assertEqual(expect, self.api.calls)
def test_data_without_checksum(self):
diff --git a/tests/v2/test_shell_v2.py b/tests/v2/test_shell_v2.py
index 38d2afb..6d0a3d9 100644
--- a/tests/v2/test_shell_v2.py
+++ b/tests/v2/test_shell_v2.py
@@ -13,17 +13,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-
import json
import mock
-import six
import testtools
-from glanceclient.common import http
-from glanceclient.common import progressbar
from glanceclient.common import utils
from glanceclient.v2 import shell as test_shell
-from tests import utils as test_utils
class ShellV2Test(testtools.TestCase):
@@ -208,16 +203,18 @@ class ShellV2Test(testtools.TestCase):
utils.print_dict.assert_called_once_with({
'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd'})
- def test_do_location_add_update_with_invalid_json_metadata(self):
- args = self._make_args({'id': 'pass',
- 'url': 'http://foo/bar',
- 'metadata': '{1, 2, 3}'})
- self.assert_exits_with_msg(test_shell.do_location_add,
- args,
- 'Metadata is not a valid JSON object.')
- self.assert_exits_with_msg(test_shell.do_location_update,
- args,
- 'Metadata is not a valid JSON object.')
+ def test_do_explain(self):
+ input = {
+ 'page_size': 18,
+ 'id': 'pass',
+ 'schemas': 'test',
+ 'model': 'test',
+ }
+ args = self._make_args(input)
+ with mock.patch.object(utils, 'print_list'):
+ test_shell.do_explain(self.gc, args)
+
+ self.gc.schemas.get.assert_called_once_with('test')
def test_do_location_add(self):
gc = self.gc
@@ -260,19 +257,6 @@ class ShellV2Test(testtools.TestCase):
loc['metadata'])
utils.print_dict.assert_called_once_with(expect_image)
- def test_do_explain(self):
- input = {
- 'page_size': 18,
- 'id': 'pass',
- 'schemas': 'test',
- 'model': 'test',
- }
- args = self._make_args(input)
- with mock.patch.object(utils, 'print_list'):
- test_shell.do_explain(self.gc, args)
-
- self.gc.schemas.get.assert_called_once_with('test')
-
def test_image_upload(self):
args = self._make_args(
{'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False})
@@ -283,46 +267,6 @@ class ShellV2Test(testtools.TestCase):
test_shell.do_image_upload(self.gc, args)
mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024)
- def test_image_upload_with_progressbar(self):
- args = self._make_args(
- {'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': True})
-
- with mock.patch.object(self.gc.images, 'upload') as mocked_upload:
- utils.get_data_file = mock.Mock(return_value='testfile')
- utils.get_file_size = mock.Mock(return_value=8)
- mocked_upload.return_value = None
- test_shell.do_image_upload(self.gc, args)
- self.assertIsInstance(mocked_upload.call_args[0][1],
- progressbar.VerboseFileWrapper)
-
- def test_image_download(self):
- args = self._make_args(
- {'id': 'pass', 'file': 'test', 'progress': False})
-
- with mock.patch.object(self.gc.images, 'data') as mocked_data:
- resp = test_utils.FakeResponse({}, six.StringIO('CCC'))
- ret = mocked_data.return_value = http.ResponseBodyIterator(resp)
- test_shell.do_image_download(self.gc, args)
-
- mocked_data.assert_called_once_with('pass')
- utils.save_image.assert_called_once_with(ret, 'test')
-
- def test_image_download_with_progressbar(self):
- args = self._make_args(
- {'id': 'pass', 'file': 'test', 'progress': True})
-
- with mock.patch.object(self.gc.images, 'data') as mocked_data:
- resp = test_utils.FakeResponse({}, six.StringIO('CCC'))
- mocked_data.return_value = http.ResponseBodyIterator(resp)
- test_shell.do_image_download(self.gc, args)
-
- mocked_data.assert_called_once_with('pass')
- utils.save_image.assert_called_once_with(mock.ANY, 'test')
- self.assertIsInstance(
- utils.save_image.call_args[0][0],
- progressbar.VerboseIteratorWrapper
- )
-
def test_do_image_delete(self):
args = self._make_args({'id': 'pass', 'file': 'test'})
with mock.patch.object(self.gc.images, 'delete') as mocked_delete: