Handle session timeout in the VMware store
The current implementation can lead to session timeouts. This will occur when accessing the datastore directly through HTTP. This patch provides a retry mechanism by recreating the session when getting a 401 status code. Change-Id: I54cc9e30c9bc374a2cf82dec4beb9b06594835f8 Closes-Bug: #1311960
This commit is contained in:
parent
4829deeae1
commit
e5e76fffbd
@ -1,4 +1,5 @@
|
||||
# Copyright 2014 OpenStack, LLC
|
||||
# Copyright (c) 2014 VMware, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -22,10 +23,12 @@ import os
|
||||
import netaddr
|
||||
from oslo.config import cfg
|
||||
from oslo.vmware import api
|
||||
from retrying import retry
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from glance.common import exception
|
||||
from glance.openstack.common import excutils
|
||||
from glance.openstack.common import gettextutils
|
||||
import glance.openstack.common.log as logging
|
||||
import glance.store
|
||||
import glance.store.base
|
||||
@ -33,6 +36,8 @@ import glance.store.location
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
_LE = gettextutils._LE
|
||||
_LI = gettextutils._LI
|
||||
|
||||
MAX_REDIRECTS = 5
|
||||
DEFAULT_STORE_IMAGE_DIR = '/openstack_glance'
|
||||
@ -131,6 +136,13 @@ class _Reader(object):
|
||||
self.current_chunk = self.current_chunk[size:]
|
||||
return ret
|
||||
|
||||
def rewind(self):
|
||||
try:
|
||||
self.data.seek(0)
|
||||
except IOError:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Failed to rewind image content'))
|
||||
|
||||
def _get_chunk(self):
|
||||
if not self.closed:
|
||||
chunk = self.data.read(self.blocksize)
|
||||
@ -212,7 +224,20 @@ class Store(glance.store.base.Store):
|
||||
def get_schemes(self):
|
||||
return (STORE_SCHEME,)
|
||||
|
||||
def _sanity_check(self):
|
||||
if CONF.vmware_api_retry_count <= 0:
|
||||
msg = _("vmware_api_retry_count should be greater than zero")
|
||||
LOG.error(msg)
|
||||
raise exception.BadStoreConfiguration(
|
||||
store_name='vmware_datastore', reason=msg)
|
||||
if CONF.vmware_task_poll_interval <= 0:
|
||||
msg = _("vmware_task_poll_interval should be greater than zero")
|
||||
LOG.error(msg)
|
||||
raise exception.BadStoreConfiguration(
|
||||
store_name='vmware_datastore', reason=msg)
|
||||
|
||||
def configure(self):
|
||||
self._sanity_check()
|
||||
self.scheme = STORE_SCHEME
|
||||
self.server_host = self._option_get('vmware_server_host')
|
||||
self.server_username = self._option_get('vmware_server_username')
|
||||
@ -220,12 +245,7 @@ class Store(glance.store.base.Store):
|
||||
self.api_retry_count = CONF.vmware_api_retry_count
|
||||
self.task_poll_interval = CONF.vmware_task_poll_interval
|
||||
self.api_insecure = CONF.vmware_api_insecure
|
||||
self._session = api.VMwareAPISession(self.server_host,
|
||||
self.server_username,
|
||||
self.server_password,
|
||||
self.api_retry_count,
|
||||
self.task_poll_interval)
|
||||
self._service_content = self._session.vim.service_content
|
||||
self._create_session()
|
||||
|
||||
def configure_add(self):
|
||||
self.datacenter_path = CONF.vmware_datacenter_path
|
||||
@ -241,16 +261,23 @@ class Store(glance.store.base.Store):
|
||||
search_index_moref,
|
||||
inventoryPath=inventory_path)
|
||||
if ds_moref is None:
|
||||
reason = (_("Could not find datastore %(ds_name)s "
|
||||
"in datacenter %(dc_path)s")
|
||||
% {'ds_name': self.datastore_name,
|
||||
'dc_path': self.datacenter_path})
|
||||
msg = (_("Could not find datastore %(ds_name)s "
|
||||
"in datacenter %(dc_path)s")
|
||||
% {'ds_name': self.datastore_name,
|
||||
'dc_path': self.datacenter_path})
|
||||
LOG.error(msg)
|
||||
raise exception.BadStoreConfiguration(
|
||||
store_name='vmware_datastore', reason=reason)
|
||||
store_name='vmware_datastore', reason=msg)
|
||||
else:
|
||||
_datastore_info_valid = True
|
||||
self.store_image_dir = CONF.vmware_store_image_dir
|
||||
|
||||
def _create_session(self):
|
||||
self._session = api.VMwareAPISession(
|
||||
self.server_host, self.server_username, self.server_password,
|
||||
self.api_retry_count, self.task_poll_interval)
|
||||
self._service_content = self._session.vim.service_content
|
||||
|
||||
def _option_get(self, param):
|
||||
result = getattr(CONF, param)
|
||||
if not result:
|
||||
@ -266,6 +293,14 @@ class Store(glance.store.base.Store):
|
||||
cookie = list(vim_cookies)[0]
|
||||
return cookie.name + '=' + cookie.value
|
||||
|
||||
def _session_not_authenticated(exc):
|
||||
if isinstance(exc, exception.NotAuthenticated):
|
||||
LOG.info(_LI("Store session is not authenticated, retry attempt"))
|
||||
return True
|
||||
return False
|
||||
|
||||
@retry(stop_max_attempt_number=CONF.vmware_api_retry_count + 1,
|
||||
retry_on_exception=_session_not_authenticated)
|
||||
def add(self, image_id, image_file, image_size):
|
||||
"""Stores an image file with supplied identifier to the backend
|
||||
storage system and returns a tuple containing information
|
||||
@ -301,15 +336,20 @@ class Store(glance.store.base.Store):
|
||||
res = conn.getresponse()
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_('Failed to upload content of image '
|
||||
'%(image)s') % {'image': image_id})
|
||||
LOG.exception(_LE('Failed to upload content of image '
|
||||
'%(image)s'), {'image': image_id})
|
||||
|
||||
if res.status == httplib.UNAUTHORIZED:
|
||||
self._create_session()
|
||||
image_file.rewind()
|
||||
raise exception.NotAuthenticated()
|
||||
|
||||
if res.status == httplib.CONFLICT:
|
||||
raise exception.Duplicate(_("Image file %(image_id)s already "
|
||||
"exists!") % {'image_id': image_id})
|
||||
|
||||
if res.status not in (httplib.CREATED, httplib.OK):
|
||||
msg = (_('Failed to upload content of image %(image)s') %
|
||||
msg = (_LE('Failed to upload content of image %(image)s') %
|
||||
{'image': image_id})
|
||||
LOG.error(msg)
|
||||
raise exception.UnexpectedStatus(status=res.status,
|
||||
@ -325,11 +365,7 @@ class Store(glance.store.base.Store):
|
||||
:param location: `glance.store.location.Location` object, supplied
|
||||
from glance.store.location.get_location_from_uri()
|
||||
"""
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self._session.vim.client.options.transport.cookiejar)
|
||||
conn, resp, content_length = self._query(location,
|
||||
'GET',
|
||||
headers={'Cookie': cookie})
|
||||
conn, resp, content_length = self._query(location, 'GET')
|
||||
iterator = http_response_iterator(conn, resp, self.CHUNKSIZE)
|
||||
|
||||
class ResponseIndexable(glance.store.Indexable):
|
||||
@ -349,10 +385,7 @@ class Store(glance.store.base.Store):
|
||||
:param location: `glance.store.location.Location` object, supplied
|
||||
from glance.store.location.get_location_from_uri()
|
||||
"""
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self._session.vim.client.options.transport.cookiejar)
|
||||
|
||||
return self._query(location, 'HEAD', headers={'Cookie': cookie})[2]
|
||||
return self._query(location, 'HEAD')[2]
|
||||
|
||||
def delete(self, location):
|
||||
"""Takes a `glance.store.location.Location` object that indicates
|
||||
@ -380,24 +413,32 @@ class Store(glance.store.base.Store):
|
||||
self._session.wait_for_task(delete_task)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_('Failed to delete image %(image)s content.') %
|
||||
LOG.exception(_LE('Failed to delete image %(image)s content.'),
|
||||
{'image': location.image_id})
|
||||
|
||||
def _query(self, location, method, headers, depth=0):
|
||||
@retry(stop_max_attempt_number=CONF.vmware_api_retry_count + 1,
|
||||
retry_on_exception=_session_not_authenticated)
|
||||
def _query(self, location, method, depth=0):
|
||||
if depth > MAX_REDIRECTS:
|
||||
msg = ("The HTTP URL exceeded %(max_redirects)s maximum "
|
||||
"redirects." % {'max_redirects': MAX_REDIRECTS})
|
||||
"redirects.", {'max_redirects': MAX_REDIRECTS})
|
||||
LOG.debug(msg)
|
||||
raise exception.MaxRedirectsExceeded(redirects=MAX_REDIRECTS)
|
||||
loc = location.store_location
|
||||
cookie = self._build_vim_cookie_header(
|
||||
self._session.vim.client.options.transport.cookiejar)
|
||||
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(_('Failed to access image %(image)s content.') %
|
||||
LOG.exception(_LE('Failed to access image %(image)s content.'),
|
||||
{'image': location.image_id})
|
||||
if resp.status >= 400:
|
||||
if resp.status == httplib.UNAUTHORIZED:
|
||||
self._create_session()
|
||||
raise exception.NotAuthenticated()
|
||||
if resp.status == httplib.NOT_FOUND:
|
||||
msg = 'VMware datastore could not find image at URI.'
|
||||
LOG.debug(msg)
|
||||
|
@ -25,12 +25,16 @@ import ConfigParser
|
||||
import httplib
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import oslo.config.cfg
|
||||
from oslo.vmware import api
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import testtools
|
||||
|
||||
from glance.common import exception
|
||||
import glance.store.location
|
||||
import glance.store.vmware_datastore as vm_store
|
||||
import glance.tests.functional.store as store_tests
|
||||
|
||||
@ -142,3 +146,31 @@ class TestVMwareDatastoreStore(store_tests.BaseTestCase, testtools.TestCase):
|
||||
conn.getresponse()
|
||||
|
||||
return '%s://%s%s?%s' % (vm_store.STORE_SCHEME, server_ip, path, query)
|
||||
|
||||
def test_timeout(self):
|
||||
store = self.get_store()
|
||||
store._session.logout()
|
||||
image_id = str(uuid.uuid4())
|
||||
image_data = six.StringIO('XXX')
|
||||
image_checksum = 'bc9189406be84ec297464a514221406d'
|
||||
uri, add_size, add_checksum, _ = store.add(image_id, image_data, 3)
|
||||
self.assertEqual(3, add_size)
|
||||
self.assertEqual(image_checksum, add_checksum)
|
||||
|
||||
loc = glance.store.location.Location(
|
||||
self.store_name,
|
||||
store.get_store_location_class(),
|
||||
uri=uri,
|
||||
image_id=image_id)
|
||||
store._session.logout()
|
||||
get_iter, get_size = store.get(loc)
|
||||
self.assertEqual(3, get_size)
|
||||
self.assertEqual('XXX', ''.join(get_iter))
|
||||
|
||||
store._session.logout()
|
||||
image_size = store.get_size(loc)
|
||||
self.assertEqual(3, image_size)
|
||||
|
||||
store._session.logout()
|
||||
store.delete(loc)
|
||||
self.assertRaises(exception.NotFound, store.get, loc)
|
||||
|
@ -45,7 +45,8 @@ VMWARE_DATASTORE_CONF = {
|
||||
'vmware_datacenter_path': 'dc1',
|
||||
'vmware_datastore_name': 'ds1',
|
||||
'vmware_store_image_dir': '/openstack_glance',
|
||||
'vmware_api_insecure': 'True'
|
||||
'vmware_api_insecure': 'True',
|
||||
'vmware_api_retry_count': 10
|
||||
}
|
||||
|
||||
|
||||
@ -126,6 +127,8 @@ class TestStore(base.StoreClearingUnitTest):
|
||||
VMWARE_DATASTORE_CONF['vmware_datastore_name'])
|
||||
self.store.api_insecure = (
|
||||
VMWARE_DATASTORE_CONF['vmware_api_insecure'])
|
||||
self.store.api_retry_count = (
|
||||
VMWARE_DATASTORE_CONF['vmware_api_retry_count'])
|
||||
self.store._session = FakeSession()
|
||||
self.store._session.invoke_api = mock.Mock()
|
||||
self.store._session.wait_for_task = mock.Mock()
|
||||
@ -308,3 +311,48 @@ class TestStore(base.StoreClearingUnitTest):
|
||||
self.assertEqual(expected_checksum, reader.checksum.hexdigest())
|
||||
self.assertEqual(image.len, reader.size)
|
||||
self.assertTrue(reader.closed)
|
||||
|
||||
def test_sanity_check_api_retry_count(self):
|
||||
"""Test that sanity check raises if api_retry_count is <= 0."""
|
||||
vm_store.CONF.vmware_api_retry_count = -1
|
||||
self.assertRaises(exception.BadStoreConfiguration,
|
||||
self.store._sanity_check)
|
||||
vm_store.CONF.vmware_api_retry_count = 0
|
||||
self.assertRaises(exception.BadStoreConfiguration,
|
||||
self.store._sanity_check)
|
||||
vm_store.CONF.vmware_api_retry_count = 1
|
||||
try:
|
||||
self.store._sanity_check()
|
||||
except exception.BadStoreConfiguration:
|
||||
self.fail()
|
||||
|
||||
def test_sanity_check_task_poll_interval(self):
|
||||
"""Test that sanity check raises if task_poll_interval is <= 0."""
|
||||
vm_store.CONF.vmware_task_poll_interval = -1
|
||||
self.assertRaises(exception.BadStoreConfiguration,
|
||||
self.store._sanity_check)
|
||||
vm_store.CONF.vmware_task_poll_interval = 0
|
||||
self.assertRaises(exception.BadStoreConfiguration,
|
||||
self.store._sanity_check)
|
||||
vm_store.CONF.vmware_task_poll_interval = 1
|
||||
try:
|
||||
self.store._sanity_check()
|
||||
except exception.BadStoreConfiguration:
|
||||
self.fail()
|
||||
|
||||
def test_retry_count(self):
|
||||
expected_image_id = str(uuid.uuid4())
|
||||
expected_size = FIVE_KB
|
||||
expected_contents = "*" * expected_size
|
||||
image = six.StringIO(expected_contents)
|
||||
self.store._create_session = mock.Mock()
|
||||
with mock.patch('httplib.HTTPConnection') as HttpConn:
|
||||
HttpConn.return_value = FakeHTTPConnection(status=401)
|
||||
try:
|
||||
location, size, checksum, _ = self.store.add(expected_image_id,
|
||||
image,
|
||||
expected_size)
|
||||
except exception.NotAuthenticated:
|
||||
pass
|
||||
self.assertEqual(VMWARE_DATASTORE_CONF['vmware_api_retry_count'] + 1,
|
||||
self.store._create_session.call_count)
|
||||
|
@ -30,7 +30,7 @@ posix_ipc
|
||||
python-swiftclient>=2.0.2
|
||||
|
||||
# For VMware storage backed.
|
||||
oslo.vmware>=0.3 # Apache-2.0
|
||||
oslo.vmware>=0.4 # Apache-2.0
|
||||
|
||||
# For paste.util.template used in keystone.common.template
|
||||
Paste
|
||||
@ -44,3 +44,5 @@ pyOpenSSL>=0.11
|
||||
six>=1.7.0
|
||||
|
||||
oslo.messaging>=1.3.0
|
||||
|
||||
retrying>=1.2.1 # Apache-2.0
|
||||
|
Loading…
Reference in New Issue
Block a user