summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-02-26 13:37:24 +0000
committerGerrit Code Review <review@openstack.org>2016-02-26 13:37:24 +0000
commit0379fa4c5b567db45b54ecb989de7b42c013799d (patch)
treec3ff5fe66cac81c2d149c6c9ab7ab0f30cc4efdc
parent4d355a1a69a5da069b54de85b0352d97a1dff58e (diff)
parent91636e8b85de680ea1347b60b1c2a27022c0f26f (diff)
downloadglance_store-0379fa4c5b567db45b54ecb989de7b42c013799d.tar.gz
Merge "Switch VMWare Datastore to use Requests"
-rw-r--r--glance_store/_drivers/vmware_datastore.py220
-rw-r--r--glance_store/tests/unit/test_vmware_store.py195
-rw-r--r--glance_store/tests/utils.py4
-rw-r--r--releasenotes/notes/vmware-store-requests-369485d2cfdb6175.yaml6
4 files changed, 206 insertions, 219 deletions
diff --git a/glance_store/_drivers/vmware_datastore.py b/glance_store/_drivers/vmware_datastore.py
index 98bd0c8..819fc56 100644
--- a/glance_store/_drivers/vmware_datastore.py
+++ b/glance_store/_drivers/vmware_datastore.py
@@ -31,15 +31,20 @@ try:
from oslo_vmware import vim_util
except ImportError:
api = None
-from six.moves import http_client
+
from six.moves import urllib
+import six.moves.urllib.parse as urlparse
+import requests
+from requests import adapters
+from requests.packages.urllib3.util import retry
import six
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
from six.moves import range
import glance_store
from glance_store import capabilities
+from glance_store.common import utils
from glance_store import exceptions
from glance_store.i18n import _
from glance_store.i18n import _LE
@@ -48,6 +53,7 @@ from glance_store import location
LOG = logging.getLogger(__name__)
+CHUNKSIZE = 1024 * 64 # 64kB
MAX_REDIRECTS = 5
DEFAULT_STORE_IMAGE_DIR = '/openstack_glance'
DS_URL_PREFIX = '/folder'
@@ -138,49 +144,6 @@ class _Reader(object):
return self._size
-class _ChunkReader(_Reader):
-
- def __init__(self, data, verifier=None, blocksize=8192):
- self.blocksize = blocksize
- self.current_chunk = b""
- self.closed = False
- super(_ChunkReader, self).__init__(data, verifier)
-
- def read(self, size=None):
- ret = b""
- while size is None or size >= len(self.current_chunk):
- ret += self.current_chunk
- if size is not None:
- size -= len(self.current_chunk)
- if self.closed:
- self.current_chunk = b""
- break
- self._get_chunk()
- else:
- ret += self.current_chunk[:size]
- self.current_chunk = self.current_chunk[size:]
- return ret
-
- def _get_chunk(self):
- if not self.closed:
- chunk = self.data.read(self.blocksize)
- chunk_len = len(chunk)
- self._size += chunk_len
- self.checksum.update(chunk)
- if self.verifier:
- self.verifier.update(chunk)
- if chunk:
- if six.PY3:
- size_header = ('%x\r\n' % chunk_len).encode('ascii')
- self.current_chunk = b''.join((size_header, chunk,
- b'\r\n'))
- else:
- self.current_chunk = b'%x\r\n%s\r\n' % (chunk_len, chunk)
- else:
- self.current_chunk = b'0\r\n\r\n'
- self.closed = True
-
-
class StoreLocation(location.StoreLocation):
"""Class describing an VMware URI.
@@ -248,6 +211,16 @@ class StoreLocation(location.StoreLocation):
if ds_name:
self.datastore_name = ds_name[0]
+ @property
+ def https_url(self):
+ """
+ Creates a https url that can be used to upload/download data from a
+ vmware store.
+ """
+ parsed_url = urlparse.urlparse(self.get_uri())
+ new_url = parsed_url._replace(scheme='https')
+ return urlparse.urlunparse(new_url)
+
class Store(glance_store.Store):
"""An implementation of the VMware datastore adapter."""
@@ -445,15 +418,13 @@ class Store(glance_store.Store):
are 201 Created and 200 OK.
"""
ds = self.select_datastore(image_size)
+ image_file = _Reader(image_file, verifier)
+ headers = {}
if image_size > 0:
- headers = {'Content-Length': image_size}
- image_file = _Reader(image_file, verifier)
+ headers.update({'Content-Length': image_size})
+ data = image_file
else:
- # NOTE (arnaud): use chunk encoding when the image is still being
- # generated by the server (ex: stream optimized disks generated by
- # Nova).
- headers = {'Transfer-Encoding': 'chunked'}
- image_file = _ChunkReader(image_file, verifier)
+ data = utils.chunkiter(image_file, CHUNKSIZE)
loc = StoreLocation({'scheme': self.scheme,
'server_host': self.server_host,
'image_dir': self.store_image_dir,
@@ -463,13 +434,15 @@ class Store(glance_store.Store):
# NOTE(arnaud): use a decorator when the config is not tied to self
cookie = self._build_vim_cookie_header(True)
headers = dict(headers)
- headers['Cookie'] = cookie
- conn_class = self._get_http_conn_class()
- conn = conn_class(loc.server_host)
- url = urllib.parse.quote('%s?%s' % (loc.path, loc.query))
+ headers.update({'Cookie': cookie})
+ session = new_session(self.api_insecure)
+
+ url = loc.https_url
try:
- conn.request('PUT', url, image_file, headers)
+ response = session.put(url, data=data, headers=headers)
except IOError as e:
+ # TODO(sigmavirus24): Figure out what the new exception type would
+ # be in requests.
# When a session is not authenticated, the socket is closed by
# the server after sending the response. http_client has an open
# issue with https that raises Broken Pipe
@@ -482,17 +455,19 @@ class Store(glance_store.Store):
'url': url,
'e': e}
LOG.error(msg)
+ raise exceptions.BackendException(msg)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Failed to upload content of image '
'%(image)s'), {'image': image_id})
- res = conn.getresponse()
- if res.status == http_client.CONFLICT:
+
+ res = response.raw
+ if res.status == requests.codes.conflict:
raise exceptions.Duplicate(_("Image file %(image_id)s already "
"exists!") %
{'image_id': image_id})
- if res.status not in (http_client.CREATED, http_client.OK):
+ if res.status not in (requests.codes.created, requests.codes.ok):
msg = (_LE('Failed to upload content of image %(image)s. '
'The request returned an unexpected status: %(status)s.'
'\nThe response body:\n%(body)s') %
@@ -534,7 +509,15 @@ class Store(glance_store.Store):
:param location: `glance_store.location.Location` object, supplied
from glance_store.location.get_location_from_uri()
"""
- return self._query(location, 'HEAD')[2]
+ conn = None
+ try:
+ conn, resp, size = self._query(location, 'HEAD')
+ return size
+ finally:
+ # NOTE(sabari): Close the connection as the request was made with
+ # stream=True.
+ if conn is not None:
+ conn.close()
@capabilities.check
def delete(self, location, context=None):
@@ -566,30 +549,59 @@ class Store(glance_store.Store):
LOG.exception(_LE('Failed to delete image %(image)s '
'content.') % {'image': location.image_id})
- def _query(self, location, method, depth=0):
- if depth > MAX_REDIRECTS:
+ def _query(self, location, method):
+ session = new_session(self.api_insecure)
+ loc = location.store_location
+ redirects_followed = 0
+ # TODO(sabari): The redirect logic was added to handle cases when the
+ # backend redirects http url's to https. But the store never makes a
+ # http request and hence this can be safely removed.
+ while redirects_followed < MAX_REDIRECTS:
+ conn, resp = self._retry_request(session, method, location)
+
+ # NOTE(sigmavirus24): _retry_request handles 4xx and 5xx errors so
+ # if the response is not a redirect, we can return early.
+ if not conn.is_redirect:
+ break
+
+ redirects_followed += 1
+
+ location_header = conn.headers.get('location')
+ if location_header:
+ if resp.status not in (301, 302):
+ reason = (_("The HTTP URL %(path)s attempted to redirect "
+ "with an invalid %(status)s status code.")
+ % {'path': loc.path, 'status': resp.status})
+ LOG.info(reason)
+ raise exceptions.BadStoreUri(message=reason)
+ conn.close()
+ location = self._new_location(location, location_header)
+ else:
+ # NOTE(sigmavirus24): We exceeded the maximum number of redirects
msg = ("The HTTP URL exceeded %(max_redirects)s maximum "
"redirects.", {'max_redirects': MAX_REDIRECTS})
LOG.debug(msg)
raise exceptions.MaxRedirectsExceeded(redirects=MAX_REDIRECTS)
+
+ content_length = int(resp.getheader('content-length', 0))
+
+ return (conn, resp, content_length)
+
+ def _retry_request(self, session, method, location):
loc = location.store_location
# NOTE(arnaud): use a decorator when the config is not tied to self
for i in range(self.api_retry_count + 1):
cookie = self._build_vim_cookie_header()
headers = {'Cookie': cookie}
- try:
- conn = self._get_http_conn(method, loc, headers)
- resp = conn.getresponse()
- except Exception:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Failed to access image %(image)s '
- 'content.') % {'image':
- location.image_id})
+ conn = session.request(method, loc.https_url, headers=headers,
+ stream=True)
+ resp = conn.raw
+
if resp.status >= 400:
- if resp.status == http_client.UNAUTHORIZED:
+ if resp.status == requests.codes.unauthorized:
self.reset_session()
continue
- if resp.status == http_client.NOT_FOUND:
+ if resp.status == requests.codes.not_found:
reason = _('VMware datastore could not find image at URI.')
LOG.info(reason)
raise exceptions.NotFound(message=reason)
@@ -598,34 +610,36 @@ class Store(glance_store.Store):
LOG.debug(msg)
raise exceptions.BadStoreUri(msg)
break
- location_header = resp.getheader('location')
- if location_header:
- if resp.status not in (301, 302):
- reason = (_("The HTTP URL %(path)s attempted to redirect "
- "with an invalid %(status)s status code.")
- % {'path': loc.path, 'status': resp.status})
- LOG.info(reason)
- raise exceptions.BadStoreUri(message=reason)
- location_class = glance_store.location.Location
- new_loc = location_class(location.store_name,
- location.store_location.__class__,
- uri=location_header,
- image_id=location.image_id,
- store_specs=location.store_specs)
- return self._query(new_loc, method, depth + 1)
- content_length = int(resp.getheader('content-length', 0))
-
- return (conn, resp, content_length)
-
- def _get_http_conn(self, method, loc, headers, content=None):
- conn_class = self._get_http_conn_class()
- conn = conn_class(loc.server_host)
- url = urllib.parse.quote('%s?%s' % (loc.path, loc.query))
- conn.request(method, url, content, headers)
-
- return conn
-
- def _get_http_conn_class(self):
- if self.api_insecure:
- return http_client.HTTPConnection
- return http_client.HTTPSConnection
+ return conn, resp
+
+ def _new_location(self, old_location, url):
+ store_name = old_location.store_name
+ store_class = old_location.store_location.__class__
+ image_id = old_location.image_id
+ store_specs = old_location.store_specs
+ # Note(sabari): The redirect url will have a scheme 'http(s)', but the
+ # store only accepts url with scheme 'vsphere'. Thus, replacing with
+ # store's scheme.
+ parsed_url = urlparse.urlparse(url)
+ new_url = parsed_url._replace(scheme='vsphere')
+ vsphere_url = urlparse.urlunparse(new_url)
+ return glance_store.location.Location(store_name,
+ store_class,
+ self.conf,
+ uri=vsphere_url,
+ image_id=image_id,
+ store_specs=store_specs)
+
+
+def new_session(insecure=False, total_retries=None):
+ session = requests.Session()
+ if total_retries is not None:
+ http_adapter = adapters.HTTPAdapter(
+ max_retries=retry.Retry(total=total_retries))
+ https_adapter = adapters.HTTPAdapter(
+ max_retries=retry.Retry(total=total_retries))
+ session.mount('http://', http_adapter)
+ session.mount('https://', https_adapter)
+ if insecure:
+ session.verify = False
+ return session
diff --git a/glance_store/tests/unit/test_vmware_store.py b/glance_store/tests/unit/test_vmware_store.py
index 35507a1..3f95adb 100644
--- a/glance_store/tests/unit/test_vmware_store.py
+++ b/glance_store/tests/unit/test_vmware_store.py
@@ -65,24 +65,6 @@ def format_location(host_ip, folder_name, image_id, datastores):
image_id, datacenter_path, datastore_name))
-class FakeHTTPConnection(object):
-
- def __init__(self, status=200, *args, **kwargs):
- self.status = status
- self.no_response_body = kwargs.get('no_response_body', False)
- pass
-
- def getresponse(self):
- return utils.FakeHTTPResponse(status=self.status,
- no_response_body=self.no_response_body)
-
- def request(self, *_args, **_kwargs):
- pass
-
- def close(self):
- pass
-
-
def fake_datastore_obj(*args, **kwargs):
dc_obj = oslo_datacenter.Datacenter(ref='fake-ref',
name='fake-name')
@@ -130,8 +112,8 @@ class TestStore(base.StoreBaseTest,
loc = location.get_location_from_uri(
"vsphere://127.0.0.1/folder/openstack_glance/%s"
"?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
(image_file, image_size) = self.store.get(loc)
self.assertEqual(image_size, expected_image_size)
chunks = [c for c in image_file]
@@ -146,8 +128,8 @@ class TestStore(base.StoreBaseTest,
loc = location.get_location_from_uri(
"vsphere://127.0.0.1/folder/openstack_glan"
"ce/%s?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection(status=404)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response(status_code=404)
self.assertRaises(exceptions.NotFound, self.store.get, loc)
@mock.patch.object(vm_store.Store, 'select_datastore')
@@ -170,8 +152,8 @@ class TestStore(base.StoreBaseTest,
expected_image_id,
VMWARE_DS['vmware_datastores'])
image = six.BytesIO(expected_contents)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
location, size, checksum, _ = self.store.add(expected_image_id,
image,
expected_size)
@@ -204,8 +186,8 @@ class TestStore(base.StoreBaseTest,
expected_image_id,
VMWARE_DS['vmware_datastores'])
image = six.BytesIO(expected_contents)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
location, size, checksum, _ = self.store.add(expected_image_id,
image, 0)
self.assertEqual(utils.sort_url_by_qs_keys(expected_location),
@@ -222,14 +204,14 @@ class TestStore(base.StoreBaseTest,
size = FIVE_KB
contents = b"*" * size
image = six.BytesIO(contents)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
self.store.add(image_id, image, size, verifier=verifier)
fake_reader.assert_called_with(image, verifier)
@mock.patch.object(vm_store.Store, 'select_datastore')
- @mock.patch('glance_store._drivers.vmware_datastore._ChunkReader')
+ @mock.patch('glance_store._drivers.vmware_datastore._Reader')
def test_add_with_verifier_size_zero(self, fake_reader, fake_select_ds):
"""Test that the verifier is passed to the _ChunkReader during add."""
verifier = mock.MagicMock(name='mock_verifier')
@@ -237,8 +219,8 @@ class TestStore(base.StoreBaseTest,
size = FIVE_KB
contents = b"*" * size
image = six.BytesIO(contents)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
self.store.add(image_id, image, 0, verifier=verifier)
fake_reader.assert_called_with(image, verifier)
@@ -249,12 +231,12 @@ class TestStore(base.StoreBaseTest,
loc = location.get_location_from_uri(
"vsphere://127.0.0.1/folder/openstack_glance/%s?"
"dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
vm_store.Store._service_content = mock.Mock()
self.store.delete(loc)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection(status=404)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response(status_code=404)
self.assertRaises(exceptions.NotFound, self.store.get, loc)
@mock.patch('oslo_vmware.api.VMwareAPISession')
@@ -278,8 +260,8 @@ class TestStore(base.StoreBaseTest,
loc = location.get_location_from_uri(
"vsphere://127.0.0.1/folder/openstack_glance/%s"
"?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection()
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response()
image_size = self.store.get_size(loc)
self.assertEqual(image_size, 31)
@@ -292,8 +274,8 @@ class TestStore(base.StoreBaseTest,
loc = location.get_location_from_uri(
"vsphere://127.0.0.1/folder/openstack_glan"
"ce/%s?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection(status=404)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response(status_code=404)
self.assertRaises(exceptions.NotFound, self.store.get_size, loc)
def test_reader_full(self):
@@ -324,76 +306,6 @@ class TestStore(base.StoreBaseTest,
reader.read()
verifier.update.assert_called_with(content)
- def test_chunkreader_image_fits_in_blocksize(self):
- """
- Test that the image file reader returns the expected chunk of data
- when the block size is larger than the image.
- """
- content = b'XXX'
- image = six.BytesIO(content)
- expected_checksum = hashlib.md5(content).hexdigest()
- reader = vm_store._ChunkReader(image)
- ret = reader.read()
- if six.PY3:
- expected_chunk = ('%x\r\n%s\r\n'
- % (len(content), content.decode('ascii')))
- expected_chunk = expected_chunk.encode('ascii')
- else:
- expected_chunk = b'%x\r\n%s\r\n' % (len(content), content)
- last_chunk = b'0\r\n\r\n'
- self.assertEqual(expected_chunk + last_chunk, ret)
- self.assertEqual(len(content), reader.size)
- self.assertEqual(expected_checksum, reader.checksum.hexdigest())
- self.assertTrue(reader.closed)
- ret = reader.read()
- self.assertEqual(len(content), reader.size)
- self.assertEqual(expected_checksum, reader.checksum.hexdigest())
- self.assertTrue(reader.closed)
- self.assertEqual(b'', ret)
-
- def test_chunkreader_image_larger_blocksize(self):
- """
- Test that the image file reader returns the expected chunks when
- the block size specified is smaller than the image.
- """
- content = b'XXX'
- image = six.BytesIO(content)
- expected_checksum = hashlib.md5(content).hexdigest()
- last_chunk = b'0\r\n\r\n'
- reader = vm_store._ChunkReader(image, blocksize=1)
- ret = reader.read()
- expected_chunk = b'1\r\nX\r\n'
- expected = (expected_chunk + expected_chunk + expected_chunk
- + last_chunk)
- self.assertEqual(expected,
- ret)
- self.assertEqual(expected_checksum, reader.checksum.hexdigest())
- self.assertEqual(len(content), reader.size)
- self.assertTrue(reader.closed)
-
- def test_chunkreader_size(self):
- """Test that the image reader takes into account the specified size."""
- content = b'XXX'
- image = six.BytesIO(content)
- expected_checksum = hashlib.md5(content).hexdigest()
- reader = vm_store._ChunkReader(image, blocksize=1)
- ret = reader.read(size=3)
- self.assertEqual(b'1\r\n', ret)
- ret = reader.read(size=1)
- self.assertEqual(b'X', ret)
- ret = reader.read()
- self.assertEqual(expected_checksum, reader.checksum.hexdigest())
- self.assertEqual(len(content), reader.size)
- self.assertTrue(reader.closed)
-
- def test_chunkreader_with_verifier(self):
- content = b'XXX'
- image = six.BytesIO(content)
- verifier = mock.MagicMock(name='mock_verifier')
- reader = vm_store._ChunkReader(image, verifier)
- reader.read(size=3)
- verifier.update.assert_called_with(content)
-
def test_sanity_check_api_retry_count(self):
"""Test that sanity check raises if api_retry_count is <= 0."""
self.store.conf.glance_store.vmware_api_retry_count = -1
@@ -475,8 +387,8 @@ class TestStore(base.StoreBaseTest,
expected_contents = b"*" * expected_size
image = six.BytesIO(expected_contents)
self.session = mock.Mock()
- with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection(status=401)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response(status_code=401)
self.assertRaises(exceptions.BackendException,
self.store.add,
expected_image_id, image, expected_size)
@@ -491,8 +403,8 @@ class TestStore(base.StoreBaseTest,
image = six.BytesIO(expected_contents)
self.session = mock.Mock()
with self._mock_http_connection() as HttpConn:
- HttpConn.return_value = FakeHTTPConnection(status=500,
- no_response_body=True)
+ HttpConn.return_value = utils.fake_response(status_code=500,
+ no_response_body=True)
self.assertRaises(exceptions.BackendException,
self.store.add,
expected_image_id, image, expected_size)
@@ -532,7 +444,7 @@ class TestStore(base.StoreBaseTest,
expected_contents = b"*" * expected_size
image = six.BytesIO(expected_contents)
self.session = mock.Mock()
- with self._mock_http_connection() as HttpConn:
+ with mock.patch('requests.Session.request') as HttpConn:
HttpConn.request.side_effect = IOError
self.assertRaises(exceptions.BackendException,
self.store.add,
@@ -660,3 +572,58 @@ class TestStore(base.StoreBaseTest,
'FindByInventoryPath',
self.store.session.vim.service_content.searchIndex,
inventoryPath=datacenter_path)
+
+ @mock.patch('oslo_vmware.api.VMwareAPISession')
+ def test_http_get_redirect(self, mock_api_session):
+ # Add two layers of redirects to the response stack, which will
+ # return the default 200 OK with the expected data after resolving
+ # both redirects.
+ redirect1 = {"location": "https://example.com?dsName=ds1&dcPath=dc1"}
+ redirect2 = {"location": "https://example.com?dsName=ds2&dcPath=dc2"}
+ responses = [utils.fake_response(),
+ utils.fake_response(status_code=302, headers=redirect1),
+ utils.fake_response(status_code=301, headers=redirect2)]
+
+ def getresponse(*args, **kwargs):
+ return responses.pop()
+
+ expected_image_size = 31
+ expected_returns = ['I am a teapot, short and stout\n']
+ loc = location.get_location_from_uri(
+ "vsphere://127.0.0.1/folder/openstack_glance/%s"
+ "?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.side_effect = getresponse
+ (image_file, image_size) = self.store.get(loc)
+ self.assertEqual(image_size, expected_image_size)
+ chunks = [c for c in image_file]
+ self.assertEqual(expected_returns, chunks)
+
+ @mock.patch('oslo_vmware.api.VMwareAPISession')
+ def test_http_get_max_redirects(self, mock_api_session):
+ redirect = {"location": "https://example.com?dsName=ds1&dcPath=dc1"}
+ responses = ([utils.fake_response(status_code=302, headers=redirect)]
+ * (vm_store.MAX_REDIRECTS + 1))
+
+ def getresponse(*args, **kwargs):
+ return responses.pop()
+
+ loc = location.get_location_from_uri(
+ "vsphere://127.0.0.1/folder/openstack_glance/%s"
+ "?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.side_effect = getresponse
+ self.assertRaises(exceptions.MaxRedirectsExceeded, self.store.get,
+ loc)
+
+ @mock.patch('oslo_vmware.api.VMwareAPISession')
+ def test_http_get_redirect_invalid(self, mock_api_session):
+ redirect = {"location": "https://example.com?dsName=ds1&dcPath=dc1"}
+
+ loc = location.get_location_from_uri(
+ "vsphere://127.0.0.1/folder/openstack_glance/%s"
+ "?dsName=ds1&dcPath=dc1" % FAKE_UUID, conf=self.conf)
+ with mock.patch('requests.Session.request') as HttpConn:
+ HttpConn.return_value = utils.fake_response(status_code=307,
+ headers=redirect)
+ self.assertRaises(exceptions.BadStoreUri, self.store.get, loc)
diff --git a/glance_store/tests/utils.py b/glance_store/tests/utils.py
index 09e0d35..2f3a90f 100644
--- a/glance_store/tests/utils.py
+++ b/glance_store/tests/utils.py
@@ -67,9 +67,9 @@ class FakeHTTPResponse(object):
self.data.close()
-def fake_response(status_code=200, headers=None, content=None):
+def fake_response(status_code=200, headers=None, content=None, **kwargs):
r = requests.models.Response()
r.status_code = status_code
r.headers = headers or {}
- r.raw = FakeHTTPResponse(status_code, headers, content)
+ r.raw = FakeHTTPResponse(status_code, headers, content, kwargs)
return r
diff --git a/releasenotes/notes/vmware-store-requests-369485d2cfdb6175.yaml b/releasenotes/notes/vmware-store-requests-369485d2cfdb6175.yaml
new file mode 100644
index 0000000..060f3e5
--- /dev/null
+++ b/releasenotes/notes/vmware-store-requests-369485d2cfdb6175.yaml
@@ -0,0 +1,6 @@
+---
+security:
+ - Previously the VMWare Datastore was using HTTPS Connections from httplib
+ which do not verify the connection. By switching to using requests library
+ the VMware storage backend now verifies HTTPS connection to vCenter server
+ and thus addresses the vulnerabilities described in OSSN-0033.