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
This commit is contained in:
AmalaBasha 2014-07-01 14:45:12 +05:30
parent 1db17aaad9
commit dbb242b776
22 changed files with 744 additions and 1063 deletions

@ -14,16 +14,11 @@
# under the License. # under the License.
import copy import copy
import errno
import hashlib
import logging import logging
import posixpath
import socket import socket
import ssl
import struct
import requests
import six import six
from six.moves import http_client
from six.moves.urllib import parse from six.moves.urllib import parse
try: try:
@ -36,9 +31,7 @@ if not hasattr(parse, 'parse_qsl'):
import cgi import cgi
parse.parse_qsl = cgi.parse_qsl parse.parse_qsl = cgi.parse_qsl
import OpenSSL from glanceclient.common import https
from glanceclient.common import utils
from glanceclient import exc from glanceclient import exc
from glanceclient.openstack.common import importutils from glanceclient.openstack.common import importutils
from glanceclient.openstack.common import network_utils from glanceclient.openstack.common import network_utils
@ -46,48 +39,15 @@ from glanceclient.openstack.common import strutils
osprofiler_web = importutils.try_import("osprofiler.web") 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__) LOG = logging.getLogger(__name__)
USER_AGENT = 'python-glanceclient' USER_AGENT = 'python-glanceclient'
CHUNKSIZE = 1024 * 64 # 64kB CHUNKSIZE = 1024 * 64 # 64kB
def to_bytes(s):
if isinstance(s, six.string_types):
return six.b(s)
else:
return s
class HTTPClient(object): class HTTPClient(object):
def __init__(self, endpoint, **kwargs): def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint 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.identity_headers = kwargs.get('identity_headers')
self.auth_token = kwargs.get('token') self.auth_token = kwargs.get('token')
if self.identity_headers: if self.identity_headers:
@ -95,71 +55,58 @@ class HTTPClient(object):
self.auth_token = self.identity_headers.get('X-Auth-Token') self.auth_token = self.identity_headers.get('X-Auth-Token')
del self.identity_headers['X-Auth-Token'] del self.identity_headers['X-Auth-Token']
self.session = requests.Session()
self.session.headers["User-Agent"] = USER_AGENT
self.session.headers["X-Auth-Token"] = self.auth_token
self.timeout = float(kwargs.get('timeout', 600))
if self.endpoint.startswith("https"):
compression = kwargs.get('ssl_compression', True)
if not compression:
self.session.mount("https://", https.HTTPSAdapter())
self.session.verify = kwargs.get('cacert',
not kwargs.get('insecure', True))
self.session.cert = (kwargs.get('cert_file'),
kwargs.get('key_file'))
@staticmethod @staticmethod
def parse_endpoint(endpoint): def parse_endpoint(endpoint):
return network_utils.urlsplit(endpoint) return network_utils.urlsplit(endpoint)
@staticmethod def log_curl_request(self, method, url, headers, data, kwargs):
def get_connection_class(scheme):
if scheme == 'https':
return VerifiedHTTPSConnection
else:
return http_client.HTTPConnection
@staticmethod
def get_connection_kwargs(scheme, **kwargs):
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
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)
return _kwargs
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()
def log_curl_request(self, method, url, kwargs):
curl = ['curl -i -X %s' % method] 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': if key.lower() == 'x-auth-token':
value = '*' * 3 value = '*' * 3
header = '-H \'%s: %s\'' % (key, value) header = '-H \'%s: %s\'' % (key, value)
curl.append(header) curl.append(strutils.safe_encode(header))
conn_params_fmt = [ if not self.session.verify:
('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('-k') curl.append('-k')
else:
if isinstance(self.session.verify, six.string_types):
curl.append(' --cacert %s' % self.session.verify)
if kwargs.get('body') is not None: if self.session.cert:
curl.append('-d \'%s\'' % kwargs['body']) curl.append(' --cert %s --key %s' % self.session.cert)
curl.append('%s%s' % (self.endpoint, url)) if data and isinstance(data, six.string_types):
curl.append('-d \'%s\'' % data)
if "//:" not in url:
url = '%s%s' % (self.endpoint, url)
curl.append(url)
LOG.debug(strutils.safe_encode(' '.join(curl), errors='ignore')) LOG.debug(strutils.safe_encode(' '.join(curl), errors='ignore'))
@staticmethod @staticmethod
def log_http_response(resp, body=None): 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] dump = ['\nHTTP/%.1f %s %s' % status]
headers = resp.getheaders() headers = resp.headers.items()
if 'X-Auth-Token' in headers: if 'X-Auth-Token' in headers:
headers['X-Auth-Token'] = '*' * 3 headers['X-Auth-Token'] = '*' * 3
dump.extend(['%s: %s' % (k, v) for k, v in headers]) 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)) return dict((strutils.safe_encode(h), strutils.safe_encode(v))
for h, v in six.iteritems(headers)) 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. """Send an http request with the specified characteristics.
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
as setting headers and error handling. as setting headers and error handling.
""" """
# Copy the kwargs so we can reuse the original in case of redirects # Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) headers = kwargs.pop("headers", {})
kwargs['headers'].setdefault('User-Agent', USER_AGENT) headers = headers and copy.deepcopy(headers) or {}
if osprofiler_web: # Default Content-Type is octet-stream
kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) content_type = headers.get('Content-Type', 'application/octet-stream')
if self.auth_token: def chunk_body(body):
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) chunk = body
while chunk:
chunk = body.read(CHUNKSIZE)
yield chunk
if self.identity_headers: data = kwargs.pop("data", None)
for k, v in six.iteritems(self.identity_headers): if data is not None and not isinstance(data, six.string_types):
kwargs['headers'].setdefault(k, v) 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) headers['Content-Type'] = content_type
conn = self.get_connection()
# Note(flaper87): Before letting headers / url fly, # Note(flaper87): Before letting headers / url fly,
# they should be encoded otherwise httplib will # they should be encoded otherwise httplib will
# complain. If we decide to rely on python-request # complain.
# this wont be necessary anymore. headers = self.encode_headers(headers)
kwargs['headers'] = self.encode_headers(kwargs['headers'])
try: try:
if self.endpoint_path: conn_url = "%s/%s" % (self.endpoint, url)
# NOTE(yuyangbj): this method _http_request could either be self.log_curl_request(method, conn_url, headers, data, kwargs)
# called by API layer, or be called recursively with resp = self.session.request(method,
# redirection. For example, url would be '/v1/images/detail' conn_url,
# from API layer, but url would be 'https://example.com:92/ data=data,
# v1/images/detail' from recursion. stream=True,
# See bug #1230032 and bug #1208618. headers=headers,
if url is not None: **kwargs)
all_parts = parse.urlparse(url) except requests.exceptions.Timeout as e:
if not (all_parts.scheme and all_parts.netloc): message = ("Error communicating with %(endpoint)s %(e)s" %
norm_parse = posixpath.normpath dict(url=conn_url, e=e))
url = norm_parse('/'.join([self.endpoint_path, url])) raise exc.InvalidEndpoint(message=message)
else: except requests.exceptions.ConnectionError as e:
url = self.endpoint_path message = ("Error finding address for %(url)s: %(e)s" %
dict(url=conn_url, e=e))
conn_url = parse.urlsplit(url).geturl() raise exc.CommunicationError(message=message)
# 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()
except socket.gaierror as e: except socket.gaierror as e:
message = "Error finding address for %s: %s" % ( message = "Error finding address for %s: %s" % (
self.endpoint_hostname, e) self.endpoint_hostname, e)
@ -256,357 +193,46 @@ class HTTPClient(object):
{'endpoint': endpoint, 'e': e}) {'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message) raise exc.CommunicationError(message=message)
body_iter = ResponseBodyIterator(resp) if not resp.ok:
LOG.error("Request returned failure status %s." % resp.status_code)
# Read body into string if it isn't obviously image data raise exc.from_response(resp, resp.content)
if resp.getheader('content-type', None) != 'application/octet-stream': elif resp.status_code == requests.codes.MULTIPLE_CHOICES:
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:
raise exc.from_response(resp) raise exc.from_response(resp)
content_type = resp.headers.get('Content-Type')
# 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 = 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:
body_iter = six.StringIO(content)
return resp, body_iter 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
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')
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'
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
def head(self, url, **kwargs): def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs) return self._request('HEAD', url, **kwargs)
def get(self, 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): def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs) return self._request('POST', url, **kwargs)
def put(self, url, **kwargs): def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs) return self._request('PUT', url, **kwargs)
def delete(self, url, **kwargs):
return self.raw_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs): def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs) return self._request('PATCH', url, **kwargs)
def delete(self, url, **kwargs):
class OpenSSLConnectionDelegator(object): return self._request('DELETE', url, **kwargs)
"""
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
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()

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

@ -16,6 +16,7 @@
from __future__ import print_function from __future__ import print_function
import errno import errno
import hashlib
import os import os
import re import re
import sys import sys
@ -335,3 +336,22 @@ def print_image(image_obj, max_col_width=None):
print_dict(image, max_column_width=max_col_width) print_dict(image, max_column_width=max_col_width)
else: else:
print_dict(image) 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))

@ -152,7 +152,7 @@ for obj_name in dir(sys.modules[__name__]):
def from_response(response, body=None): def from_response(response, body=None):
"""Return an instance of an HTTPException based on httplib response.""" """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: if body:
details = body.replace('\n\n', '\n') details = body.replace('\n\n', '\n')
return cls(details=details) return cls(details=details)

@ -13,10 +13,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from glanceclient.common import http from glanceclient.common.http import HTTPClient
from glanceclient.common import utils from glanceclient.common import utils
from glanceclient.v1 import image_members from glanceclient.v1.image_members import ImageMemberManager
from glanceclient.v1 import images from glanceclient.v1.images import ImageManager
class Client(object): class Client(object):
@ -31,7 +31,7 @@ class Client(object):
def __init__(self, endpoint, *args, **kwargs): def __init__(self, endpoint, *args, **kwargs):
"""Initialize a new client for the Images v1 API.""" """Initialize a new client for the Images v1 API."""
self.http_client = http.HTTPClient(utils.strip_version(endpoint), self.http_client = HTTPClient(utils.strip_version(endpoint),
*args, **kwargs) *args, **kwargs)
self.images = images.ImageManager(self.http_client) self.images = ImageManager(self.http_client)
self.image_members = image_members.ImageMemberManager(self.http_client) self.image_members = ImageMemberManager(self.http_client)

@ -34,7 +34,7 @@ class ImageMemberManager(base.ManagerWithFind):
def get(self, image, member_id): def get(self, image, member_id):
image_id = base.getid(image) image_id = base.getid(image)
url = '/v1/images/%s/members/%s' % (image_id, member_id) 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 = body['member']
member['image_id'] = image_id member['image_id'] = image_id
return ImageMember(self, member, loaded=True) return ImageMember(self, member, loaded=True)
@ -60,7 +60,7 @@ class ImageMemberManager(base.ManagerWithFind):
def _list_by_image(self, image): def _list_by_image(self, image):
image_id = base.getid(image) image_id = base.getid(image)
url = '/v1/images/%s/members' % image_id url = '/v1/images/%s/members' % image_id
resp, body = self.client.json_request('GET', url) resp, body = self.client.get(url)
out = [] out = []
for member in body['members']: for member in body['members']:
member['image_id'] = image_id member['image_id'] = image_id
@ -70,7 +70,7 @@ class ImageMemberManager(base.ManagerWithFind):
def _list_by_member(self, member): def _list_by_member(self, member):
member_id = base.getid(member) member_id = base.getid(member)
url = '/v1/shared-images/%s' % member_id url = '/v1/shared-images/%s' % member_id
resp, body = self.client.json_request('GET', url) resp, body = self.client.get(url)
out = [] out = []
for member in body['shared_images']: for member in body['shared_images']:
member['member_id'] = member_id member['member_id'] = member_id
@ -84,7 +84,7 @@ class ImageMemberManager(base.ManagerWithFind):
"""Creates an image.""" """Creates an image."""
url = '/v1/images/%s/members/%s' % (base.getid(image), member_id) url = '/v1/images/%s/members/%s' % (base.getid(image), member_id)
body = {'member': {'can_share': can_share}} body = {'member': {'can_share': can_share}}
self._put(url, json=body) self.client.put(url, data=body)
def replace(self, image, members): def replace(self, image, members):
memberships = [] memberships = []
@ -100,4 +100,4 @@ class ImageMemberManager(base.ManagerWithFind):
obj['can_share'] = member['can_share'] obj['can_share'] = member['can_share']
memberships.append(obj) memberships.append(obj)
url = '/v1/images/%s/members' % base.getid(image) url = '/v1/images/%s/members' % base.getid(image)
self.client.json_request('PUT', url, {}, {'memberships': memberships}) self.client.put(url, data={'memberships': memberships})

@ -14,10 +14,9 @@
# under the License. # under the License.
import copy import copy
import json
import six import six
from six.moves.urllib import parse import six.moves.urllib.parse as urlparse
from glanceclient.common import utils from glanceclient.common import utils
from glanceclient.openstack.common.apiclient import base from glanceclient.openstack.common.apiclient import base
@ -60,12 +59,12 @@ class ImageManager(base.ManagerWithFind):
resource_class = Image resource_class = Image
def _list(self, url, response_key, obj_class=None, body=None): 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: if obj_class is None:
obj_class = self.resource_class 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], return ([obj_class(self, res, loaded=True) for res in data if res],
resp) resp)
@ -123,13 +122,12 @@ class ImageManager(base.ManagerWithFind):
:rtype: :class:`Image` :rtype: :class:`Image`
""" """
image_id = base.getid(image) image_id = base.getid(image)
resp, body = self.client.raw_request( resp, body = self.client.head('/v1/images/%s'
'HEAD', '/v1/images/%s' % parse.quote(str(image_id))) % urlparse.quote(str(image_id)))
meta = self._image_meta_from_headers(dict(resp.getheaders())) meta = self._image_meta_from_headers(resp.headers)
return_request_id = kwargs.get('return_req_id', None) return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not 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) return Image(self, meta)
def data(self, image, do_checksum=True, **kwargs): def data(self, image, do_checksum=True, **kwargs):
@ -140,14 +138,14 @@ class ImageManager(base.ManagerWithFind):
:rtype: iterable containing image data :rtype: iterable containing image data
""" """
image_id = base.getid(image) image_id = base.getid(image)
resp, body = self.client.raw_request( resp, body = self.client.get('/v1/images/%s'
'GET', '/v1/images/%s' % parse.quote(str(image_id))) % urlparse.quote(str(image_id)))
checksum = resp.getheader('x-image-meta-checksum', None) checksum = resp.headers.get('x-image-meta-checksum', None)
if do_checksum and checksum is not 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) return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not 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 return body
@ -194,11 +192,11 @@ class ImageManager(base.ManagerWithFind):
# trying to encode them # trying to encode them
qp[param] = strutils.safe_encode(value) 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") images, resp = self._list(url, "images")
if return_request_id is not 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))
for image in images: for image in images:
if filter_owner(owner, image): if filter_owner(owner, image):
@ -253,10 +251,11 @@ class ImageManager(base.ManagerWithFind):
def delete(self, image, **kwargs): def delete(self, image, **kwargs):
"""Delete an image.""" """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) return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not 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): def create(self, **kwargs):
"""Create an image """Create an image
@ -284,12 +283,12 @@ class ImageManager(base.ManagerWithFind):
if copy_from is not None: if copy_from is not None:
hdrs['x-glance-api-copy-from'] = copy_from hdrs['x-glance-api-copy-from'] = copy_from
resp, body_iter = self.client.raw_request( resp, body = self.client.post('/v1/images',
'POST', '/v1/images', headers=hdrs, body=image_data) headers=hdrs,
body = json.loads(''.join([c for c in body_iter])) data=image_data)
return_request_id = kwargs.get('return_req_id', None) return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not 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'])) 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 hdrs['x-glance-api-copy-from'] = copy_from
url = '/v1/images/%s' % base.getid(image) url = '/v1/images/%s' % base.getid(image)
resp, body_iter = self.client.raw_request( resp, body = self.client.put(url, headers=hdrs, data=image_data)
'PUT', url, headers=hdrs, body=image_data)
body = json.loads(''.join([c for c in body_iter]))
return_request_id = kwargs.get('return_req_id', None) return_request_id = kwargs.get('return_req_id', None)
if return_request_id is not 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'])) return Image(self, self._format_image_meta_for_user(body['image']))

@ -21,25 +21,22 @@ class Controller(object):
def list(self, image_id): def list(self, image_id):
url = '/v2/images/%s/members' % 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']: for member in body['members']:
yield self.model(member) yield self.model(member)
def delete(self, image_id, member_id): def delete(self, image_id, member_id):
self.http_client.json_request('DELETE', self.http_client.delete('/v2/images/%s/members/%s' %
'/v2/images/%s/members/%s' % (image_id, member_id))
(image_id, member_id))
def update(self, image_id, member_id, member_status): def update(self, image_id, member_id, member_status):
url = '/v2/images/%s/members/%s' % (image_id, member_id) url = '/v2/images/%s/members/%s' % (image_id, member_id)
body = {'status': member_status} body = {'status': member_status}
resp, updated_member = self.http_client.json_request('PUT', url, resp, updated_member = self.http_client.put(url, data=body)
body=body)
return self.model(updated_member) return self.model(updated_member)
def create(self, image_id, member_id): def create(self, image_id, member_id):
url = '/v2/images/%s/members' % image_id url = '/v2/images/%s/members' % image_id
body = {'member': member_id} body = {'member': member_id}
resp, created_member = self.http_client.json_request('POST', url, resp, created_member = self.http_client.post(url, data=body)
body=body)
return self.model(created_member) return self.model(created_member)

@ -27,7 +27,7 @@ class Controller(object):
:param tag_value: value of the tag. :param tag_value: value of the tag.
""" """
url = '/v2/images/%s/tags/%s' % (image_id, tag_value) 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): def delete(self, image_id, tag_value):
""" """
@ -37,4 +37,4 @@ class Controller(object):
:param tag_value: tag value to be deleted. :param tag_value: tag value to be deleted.
""" """
url = '/v2/images/%s/tags/%s' % (image_id, tag_value) url = '/v2/images/%s/tags/%s' % (image_id, tag_value)
self.http_client.json_request('DELETE', url) self.http_client.delete(url)

@ -16,7 +16,6 @@
import json import json
import six import six
from six.moves.urllib import parse from six.moves.urllib import parse
import warlock import warlock
from glanceclient.common import utils from glanceclient.common import utils
@ -42,7 +41,7 @@ class Controller(object):
empty_fun = lambda *args, **kwargs: None empty_fun = lambda *args, **kwargs: None
def paginate(url): def paginate(url):
resp, body = self.http_client.json_request('GET', url) resp, body = self.http_client.get(url)
for image in body['images']: for image in body['images']:
# NOTE(bcwaldon): remove 'self' for now until we have # NOTE(bcwaldon): remove 'self' for now until we have
# an elegant way to pass it into the model constructor # an elegant way to pass it into the model constructor
@ -94,7 +93,7 @@ class Controller(object):
def get(self, image_id): def get(self, image_id):
url = '/v2/images/%s' % 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 #NOTE(bcwaldon): remove 'self' for now until we have an elegant
# way to pass it into the model constructor without conflict # way to pass it into the model constructor without conflict
body.pop('self', None) body.pop('self', None)
@ -108,11 +107,12 @@ class Controller(object):
:param do_checksum: Enable/disable checksum validation. :param do_checksum: Enable/disable checksum validation.
""" """
url = '/v2/images/%s/file' % image_id url = '/v2/images/%s/file' % image_id
resp, body = self.http_client.raw_request('GET', url) resp, body = self.http_client.get(url)
checksum = resp.getheader('content-md5', None) checksum = resp.headers.get('content-md5', None)
if do_checksum and checksum is not None: if do_checksum and checksum is not None:
body.set_checksum(checksum) return utils.integrity_iter(body, checksum)
return body else:
return body
def upload(self, image_id, image_data, image_size=None): def upload(self, image_id, image_data, image_size=None):
""" """
@ -124,14 +124,17 @@ class Controller(object):
""" """
url = '/v2/images/%s/file' % image_id url = '/v2/images/%s/file' % image_id
hdrs = {'Content-Type': 'application/octet-stream'} hdrs = {'Content-Type': 'application/octet-stream'}
self.http_client.raw_request('PUT', url, if image_size:
headers=hdrs, body = {'image_data': image_data,
body=image_data, 'image_size': image_size}
content_length=image_size) else:
body = image_data
self.http_client.put(url, headers=hdrs, data=body)
def delete(self, image_id): def delete(self, image_id):
"""Delete an image.""" """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): def create(self, **kwargs):
"""Create an image.""" """Create an image."""
@ -144,7 +147,7 @@ class Controller(object):
except warlock.InvalidOperation as e: except warlock.InvalidOperation as e:
raise TypeError(utils.exception_to_str(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 #NOTE(esheffield): remove 'self' for now until we have an elegant
# way to pass it into the model constructor without conflict # way to pass it into the model constructor without conflict
body.pop('self', None) body.pop('self', None)
@ -178,9 +181,7 @@ class Controller(object):
url = '/v2/images/%s' % image_id url = '/v2/images/%s' % image_id
hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'} hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
self.http_client.raw_request('PATCH', url, self.http_client.patch(url, headers=hdrs, data=image.patch)
headers=hdrs,
body=image.patch)
#NOTE(bcwaldon): calling image.patch doesn't clear the changes, so #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 # 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): def _send_image_update_request(self, image_id, patch_body):
url = '/v2/images/%s' % image_id url = '/v2/images/%s' % image_id
hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'} hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
self.http_client.raw_request('PATCH', url, self.http_client.patch(url, headers=hdrs, data=json.dumps(patch_body))
headers=hdrs,
body=json.dumps(patch_body))
def add_location(self, image_id, url, metadata): def add_location(self, image_id, url, metadata):
"""Add a new location entry to an image's list of locations. """Add a new location entry to an image's list of locations.

@ -81,5 +81,5 @@ class Controller(object):
def get(self, schema_name): def get(self, schema_name):
uri = '/v2/schemas/%s' % 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) return Schema(raw_schema)

@ -4,5 +4,6 @@ argparse
PrettyTable>=0.7,<0.8 PrettyTable>=0.7,<0.8
python-keystoneclient>=0.9.0 python-keystoneclient>=0.9.0
pyOpenSSL>=0.11 pyOpenSSL>=0.11
requests>=1.1
warlock>=1.0.1,<2 warlock>=1.0.1,<2
six>=1.7.0 six>=1.7.0

@ -12,18 +12,16 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
import collections
import testtools import testtools
from glanceclient import exc from glanceclient import exc
FakeResponse = collections.namedtuple('HTTPResponse', ['status'])
class TestHTTPExceptions(testtools.TestCase): class TestHTTPExceptions(testtools.TestCase):
def test_from_response(self): def test_from_response(self):
"""exc.from_response should return instance of an HTTP exception.""" """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) self.assertIsInstance(out, exc.HTTPBadRequest)

@ -12,21 +12,18 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import errno
import socket
import mock
from mox3 import mox from mox3 import mox
import requests
import six import six
from six.moves import http_client
from six.moves.urllib import parse from six.moves.urllib import parse
import tempfile
import testtools import testtools
import types
import glanceclient import glanceclient
from glanceclient.common import http from glanceclient.common import http
from glanceclient.common import utils as client_utils from glanceclient.common import https
from glanceclient import exc from glanceclient import exc
from tests import utils from tests import utils
@ -36,8 +33,7 @@ class TestClient(testtools.TestCase):
def setUp(self): def setUp(self):
super(TestClient, self).setUp() super(TestClient, self).setUp()
self.mock = mox.Mox() self.mock = mox.Mox()
self.mock.StubOutWithMock(http_client.HTTPConnection, 'request') self.mock.StubOutWithMock(requests.Session, 'request')
self.mock.StubOutWithMock(http_client.HTTPConnection, 'getresponse')
self.endpoint = 'http://example.com:9292' self.endpoint = 'http://example.com:9292'
self.client = http.HTTPClient(self.endpoint, token=u'abc123') 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 And the error should list the host and port that refused the
connection connection
""" """
http_client.HTTPConnection.request( requests.Session.request(
mox.IgnoreArg(), mox.IgnoreArg(),
mox.IgnoreArg(), mox.IgnoreArg(),
data=mox.IgnoreArg(),
headers=mox.IgnoreArg(), headers=mox.IgnoreArg(),
).AndRaise(socket.error()) stream=mox.IgnoreArg(),
).AndRaise(requests.exceptions.ConnectionError())
self.mock.ReplayAll() self.mock.ReplayAll()
try: 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 #NOTE(alaski) We expect exc.CommunicationError to be raised
# so we should never reach this point. try/except is used here # so we should never reach this point. try/except is used here
# rather than assertRaises() so that we can check the body of # rather than assertRaises() so that we can check the body of
@ -103,47 +101,23 @@ class TestClient(testtools.TestCase):
(comm_err.message, self.endpoint)) (comm_err.message, self.endpoint))
self.assertTrue(self.endpoint in comm_err.message, fail_msg) 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): def test_http_encoding(self):
http_client.HTTPConnection.request(
mox.IgnoreArg(),
mox.IgnoreArg(),
headers=mox.IgnoreArg())
# Lets fake the response # Lets fake the response
# returned by httplib # returned by requests
expected_response = b'Ok' response = 'Ok'
fake = utils.FakeResponse({}, six.BytesIO(expected_response)) headers = {"Content-Type": "text/plain"}
http_client.HTTPConnection.getresponse().AndReturn(fake) fake = utils.FakeResponse(headers, six.StringIO(response))
requests.Session.request(
mox.IgnoreArg(),
mox.IgnoreArg(),
data=mox.IgnoreArg(),
stream=mox.IgnoreArg(),
headers=mox.IgnoreArg()).AndReturn(fake)
self.mock.ReplayAll() self.mock.ReplayAll()
headers = {"test": u'ni\xf1o'} headers = {"test": u'ni\xf1o'}
resp, body = self.client.raw_request('GET', '/v1/images/detail', resp, body = self.client.get('/v1/images/detail', headers=headers)
headers=headers) self.assertEqual(resp, fake)
self.assertEqual(fake, resp)
def test_headers_encoding(self): def test_headers_encoding(self):
value = u'ni\xf1o' value = u'ni\xf1o'
@ -156,153 +130,19 @@ class TestClient(testtools.TestCase):
def test_raw_request(self): def test_raw_request(self):
" Verify the path being used for HTTP requests reflects accurately. " " Verify the path being used for HTTP requests reflects accurately. "
headers = {"Content-Type": "text/plain"}
def check_request(method, path, **kwargs): response = 'Ok'
self.assertEqual('GET', method) fake = utils.FakeResponse({}, six.StringIO(response))
# NOTE(kmcdonald): See bug #1179984 for more details. requests.Session.request(
self.assertEqual('/v1/images/detail', path)
http_client.HTTPConnection.request(
mox.IgnoreArg(), mox.IgnoreArg(),
mox.IgnoreArg(), mox.IgnoreArg(),
headers=mox.IgnoreArg()).WithSideEffects(check_request) data=mox.IgnoreArg(),
stream=mox.IgnoreArg(),
# fake the response returned by httplib headers=mox.IgnoreArg()).AndReturn(fake)
fake = utils.FakeResponse({}, six.BytesIO(b'Ok'))
http_client.HTTPConnection.getresponse().AndReturn(fake)
self.mock.ReplayAll() self.mock.ReplayAll()
resp, body = self.client.raw_request('GET', '/v1/images/detail') resp, body = self.client.get('/v1/images/detail', headers=headers)
self.assertEqual(fake, resp) self.assertEqual(resp, fake)
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)
def test_parse_endpoint(self): def test_parse_endpoint(self):
endpoint = 'http://example.com:9292' endpoint = 'http://example.com:9292'
@ -313,81 +153,84 @@ class TestClient(testtools.TestCase):
query='', fragment='') query='', fragment='')
self.assertEqual(expected, actual) 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): def test_get_connections_kwargs_http(self):
endpoint = 'http://example.com:9292' endpoint = 'http://example.com:9292'
test_client = http.HTTPClient(endpoint, token=u'adc123') test_client = http.HTTPClient(endpoint, token=u'adc123')
actual = test_client.get_connection_kwargs('http', insecure=True) self.assertEqual(test_client.timeout, 600.0)
self.assertEqual({'timeout': 600.0}, actual)
def test_get_connections_kwargs_https(self): def test_http_chunked_request(self):
endpoint = 'http://example.com:9292' # Lets fake the response
test_client = http.HTTPClient(endpoint, token=u'adc123') # returned by requests
actual = test_client.get_connection_kwargs('https', insecure=True) response = "Ok"
expected = {'cacert': None, data = six.StringIO(response)
'cert_file': None, fake = utils.FakeResponse({}, data)
'insecure': True, requests.Session.request(
'key_file': None, mox.IgnoreArg(),
'ssl_compression': True, mox.IgnoreArg(),
'timeout': 600.0} stream=mox.IgnoreArg(),
self.assertEqual(expected, actual) data=mox.IsA(types.GeneratorType),
headers=mox.IgnoreArg()).AndReturn(fake)
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)
class TestHostResolutionError(testtools.TestCase):
def setUp(self):
super(TestHostResolutionError, self).setUp()
self.mock = mox.Mox()
self.invalid_host = "example.com.incorrect_top_level_domain"
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 putrequest(self, *args, **kwargs):
raise socket.gaierror(-2, "Name or service not known")
def request(self, *args, **kwargs):
raise socket.gaierror(-2, "Name or service not known")
self.endpoint = 'http://%s:9292' % (self.invalid_host,)
self.client = http.HTTPClient(self.endpoint, token=u'abc123')
self.mock.StubOutWithMock(self.client, 'get_connection')
self.client.get_connection().AndReturn(FailingConnectionClass())
self.mock.ReplayAll() self.mock.ReplayAll()
try: headers = {"test": u'chunked_request'}
self.client.raw_request('GET', '/example/path') resp, body = self.client.post('/v1/images/',
self.fail("gaierror should be raised") headers=headers, data=data)
except exc.InvalidEndpoint as e: self.assertEqual(resp, fake)
self.assertTrue(self.invalid_host in str(e),
"exception should contain the hostname")
def tearDown(self): def test_http_json(self):
super(TestHostResolutionError, self).tearDown() data = {"test": "json_request"}
self.mock.UnsetStubs() fake = utils.FakeResponse({}, "OK")
def test_json(passed_data):
"""
This function tests whether the data
being passed to request's method is
a valid json or not.
This function will be called by pymox
:params passed_data: The data being
passed to requests.Session.request.
"""
if not isinstance(passed_data, six.string_types):
return False
try:
passed_data = json.loads(passed_data)
return data == passed_data
except (TypeError, ValueError):
return False
requests.Session.request(
mox.IgnoreArg(),
mox.IgnoreArg(),
stream=mox.IgnoreArg(),
data=mox.Func(test_json),
headers=mox.IgnoreArg()).AndReturn(fake)
self.mock.ReplayAll()
headers = {"test": u'chunked_request'}
resp, body = self.client.post('/v1/images/',
headers=headers,
data=data)
self.assertEqual(resp, fake)
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): class TestVerifiedHTTPSConnection(testtools.TestCase):
@ -396,7 +239,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
def test_setcontext_unable_to_load_cacert(self): def test_setcontext_unable_to_load_cacert(self):
"""Add this UT case with Bug#1265730.""" """Add this UT case with Bug#1265730."""
self.assertRaises(exc.SSLConfigurationError, self.assertRaises(exc.SSLConfigurationError,
http.VerifiedHTTPSConnection, https.VerifiedHTTPSConnection,
"127.0.0.1", "127.0.0.1",
None, None,
None, None,
@ -405,45 +248,3 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
None, None,
False, False,
True) 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))

@ -105,13 +105,11 @@ class ShellCacheSchemaTest(utils.TestCase):
super(ShellCacheSchemaTest, self).setUp() super(ShellCacheSchemaTest, self).setUp()
self._mock_client_setup() self._mock_client_setup()
self._mock_shell_setup() self._mock_shell_setup()
os.path.exists = mock.MagicMock()
self.cache_dir = '/dir_for_cached_schema' self.cache_dir = '/dir_for_cached_schema'
self.cache_file = self.cache_dir + '/image_schema.json' self.cache_file = self.cache_dir + '/image_schema.json'
def tearDown(self): def tearDown(self):
super(ShellCacheSchemaTest, self).tearDown() super(ShellCacheSchemaTest, self).tearDown()
os.path.exists.reset_mock()
def _mock_client_setup(self): def _mock_client_setup(self):
self.schema_dict = { self.schema_dict = {
@ -137,27 +135,8 @@ class ShellCacheSchemaTest(utils.TestCase):
return Args(args) return Args(args)
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
def test_cache_schema_gets_when_not_exists(self): @mock.patch('os.path.exists', return_value=True)
mocked_path_exists_result_lst = [True, False] def test_cache_schema_gets_when_forced(self, exists_mock):
os.path.exists.side_effect = \
lambda *args: mocked_path_exists_result_lst.pop(0)
options = {
'get_schema': False
}
self.shell._cache_schema(self._make_args(options),
home_dir=self.cache_dir)
self.assertEqual(4, open.mock_calls.__len__())
self.assertEqual(mock.call(self.cache_file, 'w'), open.mock_calls[0])
self.assertEqual(mock.call().write(json.dumps(self.schema_dict)),
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
options = { options = {
'get_schema': True 'get_schema': True
} }
@ -171,9 +150,23 @@ class ShellCacheSchemaTest(utils.TestCase):
open.mock_calls[2]) open.mock_calls[2])
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True) @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
def test_cache_schema_leaves_when_present_not_forced(self): @mock.patch('os.path.exists', side_effect=[True, False])
os.path.exists.return_value = True def test_cache_schema_gets_when_not_exists(self, exists_mock):
options = {
'get_schema': False
}
self.shell._cache_schema(self._make_args(options),
home_dir=self.cache_dir)
self.assertEqual(4, open.mock_calls.__len__())
self.assertEqual(mock.call(self.cache_file, 'w'), open.mock_calls[0])
self.assertEqual(mock.call().write(json.dumps(self.schema_dict)),
open.mock_calls[2])
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
@mock.patch('os.path.exists', return_value=True)
def test_cache_schema_leaves_when_present_not_forced(self, exists_mock):
options = { options = {
'get_schema': False '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_dir)
os.path.exists.assert_any_call(self.cache_file) 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__()) self.assertEqual(0, open.mock_calls.__len__())

@ -16,9 +16,11 @@
import os import os
from OpenSSL import crypto from OpenSSL import crypto
from requests.packages.urllib3 import poolmanager
import testtools import testtools
from glanceclient.common import http from glanceclient.common import http
from glanceclient.common import https
from glanceclient import exc from glanceclient import exc
@ -26,6 +28,26 @@ TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'var')) '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): class TestVerifiedHTTPSConnection(testtools.TestCase):
def test_ssl_init_ok(self): def test_ssl_init_ok(self):
""" """
@ -35,10 +57,10 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
key_file=key_file, key_file=key_file,
cert_file=cert_file, cert_file=cert_file,
cacert=cacert) cacert=cacert)
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
self.fail('Failed to init VerifiedHTTPSConnection.') self.fail('Failed to init VerifiedHTTPSConnection.')
@ -49,9 +71,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
cert_file=cert_file, cert_file=cert_file,
cacert=cacert) cacert=cacert)
self.fail('Failed to raise assertion.') self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
pass pass
@ -63,9 +85,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key') key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
key_file=key_file, key_file=key_file,
cacert=cacert) cacert=cacert)
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
pass pass
except Exception: except Exception:
@ -78,9 +100,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
cert_file=cert_file, cert_file=cert_file,
cacert=cacert) cacert=cacert)
self.fail('Failed to raise assertion.') self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
pass pass
@ -92,9 +114,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt') cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
cert_file=cert_file, cert_file=cert_file,
cacert=cacert) cacert=cacert)
self.fail('Failed to raise assertion.') self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
pass pass
@ -106,9 +128,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'badca.crt') cacert = os.path.join(TEST_VAR_DIR, 'badca.crt')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
cert_file=cert_file, cert_file=cert_file,
cacert=cacert) cacert=cacert)
self.fail('Failed to raise assertion.') self.fail('Failed to raise assertion.')
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
pass pass
@ -123,7 +145,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0 # The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName) self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try: 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) conn.verify_callback(None, cert, 0, 0, 1)
except Exception: except Exception:
self.fail('Unexpected exception.') self.fail('Unexpected exception.')
@ -138,7 +160,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=*.pong.example.com # The expected cert should have CN=*.pong.example.com
self.assertEqual('*.pong.example.com', cert.get_subject().commonName) self.assertEqual('*.pong.example.com', cert.get_subject().commonName)
try: 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) conn.verify_callback(None, cert, 0, 0, 1)
except Exception: except Exception:
self.fail('Unexpected exception.') self.fail('Unexpected exception.')
@ -153,13 +175,13 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0 # The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName) self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try: try:
conn = http.VerifiedHTTPSConnection('alt1.example.com', 0) conn = https.VerifiedHTTPSConnection('alt1.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1) conn.verify_callback(None, cert, 0, 0, 1)
except Exception: except Exception:
self.fail('Unexpected exception.') self.fail('Unexpected exception.')
try: try:
conn = http.VerifiedHTTPSConnection('alt2.example.com', 0) conn = https.VerifiedHTTPSConnection('alt2.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1) conn.verify_callback(None, cert, 0, 0, 1)
except Exception: except Exception:
self.fail('Unexpected exception.') self.fail('Unexpected exception.')
@ -174,19 +196,19 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0 # The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName) self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try: try:
conn = http.VerifiedHTTPSConnection('alt1.example.com', 0) conn = https.VerifiedHTTPSConnection('alt1.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1) conn.verify_callback(None, cert, 0, 0, 1)
except Exception: except Exception:
self.fail('Unexpected exception.') self.fail('Unexpected exception.')
try: try:
conn = http.VerifiedHTTPSConnection('alt2.example.com', 0) conn = https.VerifiedHTTPSConnection('alt2.example.com', 0)
conn.verify_callback(None, cert, 0, 0, 1) conn.verify_callback(None, cert, 0, 0, 1)
except Exception: except Exception:
self.fail('Unexpected exception.') self.fail('Unexpected exception.')
try: try:
conn = http.VerifiedHTTPSConnection('alt3.example.net', 0) conn = https.VerifiedHTTPSConnection('alt3.example.net', 0)
conn.verify_callback(None, cert, 0, 0, 1) conn.verify_callback(None, cert, 0, 0, 1)
self.fail('Failed to raise assertion.') self.fail('Failed to raise assertion.')
except exc.SSLCertificateError: except exc.SSLCertificateError:
@ -202,7 +224,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
# The expected cert should have CN=0.0.0.0 # The expected cert should have CN=0.0.0.0
self.assertEqual('0.0.0.0', cert.get_subject().commonName) self.assertEqual('0.0.0.0', cert.get_subject().commonName)
try: try:
conn = http.VerifiedHTTPSConnection('mismatch.example.com', 0) conn = https.VerifiedHTTPSConnection('mismatch.example.com', 0)
except Exception: except Exception:
self.fail('Failed to init VerifiedHTTPSConnection.') self.fail('Failed to init VerifiedHTTPSConnection.')
@ -220,10 +242,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
self.assertEqual('openstack.example.com', self.assertEqual('openstack.example.com',
cert.get_subject().commonName) cert.get_subject().commonName)
try: try:
conn = http.VerifiedHTTPSConnection('openstack.example.com', 0) conn = https.VerifiedHTTPSConnection('openstack.example.com', 0)
except Exception: except Exception:
self.fail('Failed to init VerifiedHTTPSConnection.') self.fail('Failed to init VerifiedHTTPSConnection.')
self.assertRaises(exc.SSLCertificateError, self.assertRaises(exc.SSLCertificateError,
conn.verify_callback, None, cert, 0, 0, 1) conn.verify_callback, None, cert, 0, 0, 1)
@ -236,7 +257,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
key_file = 'fake.key' key_file = 'fake.key'
self.assertRaises( self.assertRaises(
exc.SSLConfigurationError, exc.SSLConfigurationError,
http.VerifiedHTTPSConnection, '127.0.0.1', https.VerifiedHTTPSConnection, '127.0.0.1',
0, key_file=key_file, 0, key_file=key_file,
cert_file=cert_file, cacert=cacert) cert_file=cert_file, cacert=cacert)
@ -248,7 +269,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection( https.VerifiedHTTPSConnection(
'127.0.0.1', 0, '127.0.0.1', 0,
key_file=key_file, key_file=key_file,
cert_file=cert_file, cert_file=cert_file,
@ -264,7 +285,7 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try: try:
http.VerifiedHTTPSConnection( https.VerifiedHTTPSConnection(
'127.0.0.1', 0, '127.0.0.1', 0,
key_file=key_file, key_file=key_file,
cert_file=cert_file, cert_file=cert_file,
@ -286,9 +307,9 @@ class TestVerifiedHTTPSConnection(testtools.TestCase):
cert_file = cert_file.encode('ascii', 'strict').decode('utf-8') cert_file = cert_file.encode('ascii', 'strict').decode('utf-8')
cacert = cacert.encode('ascii', 'strict').decode('utf-8') cacert = cacert.encode('ascii', 'strict').decode('utf-8')
try: try:
http.VerifiedHTTPSConnection('127.0.0.1', 0, https.VerifiedHTTPSConnection('127.0.0.1', 0,
key_file=key_file, key_file=key_file,
cert_file=cert_file, cert_file=cert_file,
cacert=cacert) cacert=cacert)
except exc.SSLConfigurationError: except exc.SSLConfigurationError:
self.fail('Failed to init VerifiedHTTPSConnection.') self.fail('Failed to init VerifiedHTTPSConnection.')

@ -14,64 +14,53 @@
# under the License. # under the License.
import copy import copy
import requests import json
import six import six
import testtools import testtools
from glanceclient.common import http
class FakeAPI(object): class FakeAPI(object):
def __init__(self, fixtures): def __init__(self, fixtures):
self.fixtures = fixtures self.fixtures = fixtures
self.calls = [] self.calls = []
def _request(self, method, url, headers=None, body=None, def _request(self, method, url, headers=None, data=None,
content_length=None): content_length=None):
call = (method, url, headers or {}, body) call = (method, url, headers or {}, data)
if content_length is not None: if content_length is not None:
call = tuple(list(call) + [content_length]) call = tuple(list(call) + [content_length])
self.calls.append(call) self.calls.append(call)
return self.fixtures[url][method] fixture = self.fixtures[url][method]
def raw_request(self, *args, **kwargs): data = fixture[1]
fixture = self._request(*args, **kwargs) if isinstance(fixture[1], six.string_types):
resp = FakeResponse(fixture[0], six.StringIO(fixture[1])) try:
body_iter = http.ResponseBodyIterator(resp) data = json.loads(fixture[1])
return resp, body_iter except ValueError:
data = six.StringIO(fixture[1])
def json_request(self, *args, **kwargs): return FakeResponse(fixture[0], fixture[1]), data
fixture = self._request(*args, **kwargs)
return FakeResponse(fixture[0]), fixture[1]
def client_request(self, method, url, **kwargs): def get(self, *args, **kwargs):
if 'json' in kwargs and 'body' not in kwargs: return self._request('GET', *args, **kwargs)
kwargs['body'] = kwargs.pop('json')
resp, body = self.json_request(method, url, **kwargs)
resp.json = lambda: body
resp.content = bool(body)
return resp
def head(self, url, **kwargs): def post(self, *args, **kwargs):
return self.client_request("HEAD", url, **kwargs) return self._request('POST', *args, **kwargs)
def get(self, url, **kwargs): def put(self, *args, **kwargs):
return self.client_request("GET", url, **kwargs) return self._request('PUT', *args, **kwargs)
def post(self, url, **kwargs): def patch(self, *args, **kwargs):
return self.client_request("POST", url, **kwargs) return self._request('PATCH', *args, **kwargs)
def put(self, url, **kwargs): def delete(self, *args, **kwargs):
return self.client_request("PUT", url, **kwargs) return self._request('DELETE', *args, **kwargs)
def delete(self, url, **kwargs): def head(self, *args, **kwargs):
return self.raw_request("DELETE", url, **kwargs) return self._request('HEAD', *args, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
class FakeResponse(object): class RawRequest(object):
def __init__(self, headers, body=None, def __init__(self, headers, body=None,
version=1.0, status=200, reason="Ok"): version=1.0, status=200, reason="Ok"):
""" """
@ -97,36 +86,55 @@ class FakeResponse(object):
return self.body.read(amt) return self.body.read(amt)
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)
def read(self, amt):
return self.body.read(amt)
@property
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): class TestCase(testtools.TestCase):
TEST_REQUEST_BASE = { TEST_REQUEST_BASE = {
'config': {'danger_mode': False}, 'config': {'danger_mode': False},
'verify': True} 'verify': True}
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__
@property
def text(self):
return self._text
class FakeTTYStdout(six.StringIO): class FakeTTYStdout(six.StringIO):
"""A Fake stdout that try to emulate a TTY device as much as possible.""" """A Fake stdout that try to emulate a TTY device as much as possible."""

@ -932,7 +932,7 @@ class ParameterFakeAPI(utils.FakeAPI):
}, },
]} ]}
def json_request(self, method, url, **kwargs): def get(self, url, **kwargs):
self.url = url self.url = url
return utils.FakeResponse({}), ParameterFakeAPI.image_list return utils.FakeResponse({}), ParameterFakeAPI.image_list

@ -257,7 +257,7 @@ class LegacyShellV1Test(testtools.TestCase):
args = Image() args = Image()
gc = client.Client('1', 'http://is.invalid') gc = client.Client('1', 'http://is.invalid')
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, test_shell.do_update, gc, args) exc.CommunicationError, test_shell.do_update, gc, args)
def test_do_update(self): def test_do_update(self):
class Image(): class Image():

@ -224,81 +224,81 @@ class ShellInvalidEndpointandParameterTest(utils.TestCase):
def test_image_list_invalid_endpoint(self): def test_image_list_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, self.run_command, 'image-list') exc.CommunicationError, self.run_command, 'image-list')
def test_image_details_invalid_endpoint_legacy(self): def test_image_details_invalid_endpoint_legacy(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, self.run_command, 'details') exc.CommunicationError, self.run_command, 'details')
def test_image_update_invalid_endpoint_legacy(self): def test_image_update_invalid_endpoint_legacy(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'update {"name":""test}') self.run_command, 'update {"name":""test}')
def test_image_index_invalid_endpoint_legacy(self): def test_image_index_invalid_endpoint_legacy(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'index') self.run_command, 'index')
def test_image_create_invalid_endpoint(self): def test_image_create_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'image-create') self.run_command, 'image-create')
def test_image_delete_invalid_endpoint(self): def test_image_delete_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'image-delete <fake>') self.run_command, 'image-delete <fake>')
def test_image_download_invalid_endpoint(self): def test_image_download_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'image-download <fake>') self.run_command, 'image-download <fake>')
def test_image_members_invalid_endpoint(self): def test_image_members_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'image-members fake_id') self.run_command, 'image-members fake_id')
def test_members_list_invalid_endpoint(self): def test_members_list_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'member-list --image-id fake') self.run_command, 'member-list --image-id fake')
def test_member_replace_invalid_endpoint(self): def test_member_replace_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'members-replace image_id member_id') self.run_command, 'members-replace image_id member_id')
def test_image_show_invalid_endpoint_legacy(self): def test_image_show_invalid_endpoint_legacy(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, self.run_command, 'show image') exc.CommunicationError, self.run_command, 'show image')
def test_image_show_invalid_endpoint(self): def test_image_show_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'image-show --human-readable <IMAGE_ID>') self.run_command, 'image-show --human-readable <IMAGE_ID>')
def test_member_images_invalid_endpoint_legacy(self): def test_member_images_invalid_endpoint_legacy(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, 'member-images member_id') self.run_command, 'member-images member_id')
def test_member_create_invalid_endpoint(self): def test_member_create_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, self.run_command,
'member-create --can-share <IMAGE_ID> <TENANT_ID>') 'member-create --can-share <IMAGE_ID> <TENANT_ID>')
def test_member_delete_invalid_endpoint(self): def test_member_delete_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, self.run_command,
'member-delete <IMAGE_ID> <TENANT_ID>') 'member-delete <IMAGE_ID> <TENANT_ID>')
def test_member_add_invalid_endpoint(self): def test_member_add_invalid_endpoint(self):
self.assertRaises( self.assertRaises(
exc.InvalidEndpoint, exc.CommunicationError,
self.run_command, self.run_command,
'member-add <IMAGE_ID> <TENANT_ID>') 'member-add <IMAGE_ID> <TENANT_ID>')

@ -514,9 +514,11 @@ class TestController(testtools.TestCase):
image_data = 'CCC' image_data = 'CCC'
image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
self.controller.upload(image_id, image_data, image_size=3) 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, expect = [('PUT', '/v2/images/%s/file' % image_id,
{'Content-Type': 'application/octet-stream'}, {'Content-Type': 'application/octet-stream'},
image_data, 3)] body)]
self.assertEqual(expect, self.api.calls) self.assertEqual(expect, self.api.calls)
def test_data_without_checksum(self): def test_data_without_checksum(self):

@ -13,17 +13,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json import json
import mock import mock
import six
import testtools import testtools
from glanceclient.common import http
from glanceclient.common import progressbar
from glanceclient.common import utils from glanceclient.common import utils
from glanceclient.v2 import shell as test_shell from glanceclient.v2 import shell as test_shell
from tests import utils as test_utils
class ShellV2Test(testtools.TestCase): class ShellV2Test(testtools.TestCase):
@ -208,16 +203,18 @@ class ShellV2Test(testtools.TestCase):
utils.print_dict.assert_called_once_with({ utils.print_dict.assert_called_once_with({
'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd'}) 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd'})
def test_do_location_add_update_with_invalid_json_metadata(self): def test_do_explain(self):
args = self._make_args({'id': 'pass', input = {
'url': 'http://foo/bar', 'page_size': 18,
'metadata': '{1, 2, 3}'}) 'id': 'pass',
self.assert_exits_with_msg(test_shell.do_location_add, 'schemas': 'test',
args, 'model': 'test',
'Metadata is not a valid JSON object.') }
self.assert_exits_with_msg(test_shell.do_location_update, args = self._make_args(input)
args, with mock.patch.object(utils, 'print_list'):
'Metadata is not a valid JSON object.') test_shell.do_explain(self.gc, args)
self.gc.schemas.get.assert_called_once_with('test')
def test_do_location_add(self): def test_do_location_add(self):
gc = self.gc gc = self.gc
@ -260,19 +257,6 @@ class ShellV2Test(testtools.TestCase):
loc['metadata']) loc['metadata'])
utils.print_dict.assert_called_once_with(expect_image) 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): def test_image_upload(self):
args = self._make_args( args = self._make_args(
{'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False}) {'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) test_shell.do_image_upload(self.gc, args)
mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024) 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): def test_do_image_delete(self):
args = self._make_args({'id': 'pass', 'file': 'test'}) args = self._make_args({'id': 'pass', 'file': 'test'})
with mock.patch.object(self.gc.images, 'delete') as mocked_delete: with mock.patch.object(self.gc.images, 'delete') as mocked_delete: