Merge "Use session in cinderclient"
This commit is contained in:
commit
c51673f384
|
@ -139,6 +139,10 @@ class NovaKeystoneContext(wsgi.Middleware):
|
|||
raise webob.exc.HTTPInternalServerError(
|
||||
_('Invalid service catalog json.'))
|
||||
|
||||
# NOTE(jamielennox): This is a full auth plugin set by auth_token
|
||||
# middleware in newer versions.
|
||||
user_auth_plugin = req.environ.get('keystone.token_auth')
|
||||
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
user_name=user_name,
|
||||
|
@ -147,7 +151,8 @@ class NovaKeystoneContext(wsgi.Middleware):
|
|||
auth_token=auth_token,
|
||||
remote_address=remote_address,
|
||||
service_catalog=service_catalog,
|
||||
request_id=req_id)
|
||||
request_id=req_id,
|
||||
user_auth_plugin=user_auth_plugin)
|
||||
|
||||
req.environ['nova.context'] = ctx
|
||||
return self.application
|
||||
|
|
|
@ -38,6 +38,7 @@ from cinderclient import exceptions as cinder_exception
|
|||
import eventlet.event
|
||||
from eventlet import greenthread
|
||||
import eventlet.timeout
|
||||
from keystoneclient import exceptions as keystone_exception
|
||||
from oslo.config import cfg
|
||||
from oslo import messaging
|
||||
from oslo.serialization import jsonutils
|
||||
|
@ -2403,7 +2404,8 @@ class ComputeManager(manager.Manager):
|
|||
except exception.VolumeNotFound as exc:
|
||||
LOG.debug('Ignoring VolumeNotFound: %s', exc,
|
||||
instance=instance)
|
||||
except cinder_exception.EndpointNotFound as exc:
|
||||
except (cinder_exception.EndpointNotFound,
|
||||
keystone_exception.EndpointNotFound) as exc:
|
||||
LOG.warn(_LW('Ignoring EndpointNotFound: %s'), exc,
|
||||
instance=instance)
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
import copy
|
||||
|
||||
from keystoneclient import auth
|
||||
from keystoneclient import service_catalog
|
||||
from oslo.utils import timeutils
|
||||
import six
|
||||
|
||||
|
@ -33,6 +35,32 @@ from nova import policy
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _ContextAuthPlugin(auth.BaseAuthPlugin):
|
||||
"""A keystoneclient auth plugin that uses the values from the Context.
|
||||
|
||||
Ideally we would use the plugin provided by auth_token middleware however
|
||||
this plugin isn't serialized yet so we construct one from the serialized
|
||||
auth data.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_token, sc):
|
||||
super(_ContextAuthPlugin, self).__init__()
|
||||
|
||||
self.auth_token = auth_token
|
||||
sc = {'serviceCatalog': sc}
|
||||
self.service_catalog = service_catalog.ServiceCatalogV2(sc)
|
||||
|
||||
def get_token(self, *args, **kwargs):
|
||||
return self.auth_token
|
||||
|
||||
def get_endpoint(self, session, service_type=None, interface=None,
|
||||
region_name=None, service_name=None, **kwargs):
|
||||
return self.service_catalog.url_for(service_type=service_type,
|
||||
service_name=service_name,
|
||||
endpoint_type=interface,
|
||||
region_name=region_name)
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
"""Security context and request information.
|
||||
|
||||
|
@ -44,15 +72,18 @@ class RequestContext(object):
|
|||
roles=None, remote_address=None, timestamp=None,
|
||||
request_id=None, auth_token=None, overwrite=True,
|
||||
quota_class=None, user_name=None, project_name=None,
|
||||
service_catalog=None, instance_lock_checked=False, **kwargs):
|
||||
service_catalog=None, instance_lock_checked=False,
|
||||
user_auth_plugin=None, **kwargs):
|
||||
""":param read_deleted: 'no' indicates deleted records are hidden,
|
||||
'yes' indicates deleted records are visible,
|
||||
'only' indicates that *only* deleted records are visible.
|
||||
|
||||
|
||||
:param overwrite: Set to False to ensure that the greenthread local
|
||||
copy of the index is not overwritten.
|
||||
|
||||
:param user_auth_plugin: The auth plugin for the current request's
|
||||
authentication data.
|
||||
|
||||
:param kwargs: Extra arguments that might be present, but we ignore
|
||||
because they possibly came in from older rpc messages.
|
||||
"""
|
||||
|
@ -92,11 +123,18 @@ class RequestContext(object):
|
|||
self.user_name = user_name
|
||||
self.project_name = project_name
|
||||
self.is_admin = is_admin
|
||||
self.user_auth_plugin = user_auth_plugin
|
||||
if self.is_admin is None:
|
||||
self.is_admin = policy.check_is_admin(self)
|
||||
if overwrite or not hasattr(local.store, 'context'):
|
||||
self.update_store()
|
||||
|
||||
def get_auth_plugin(self):
|
||||
if self.user_auth_plugin:
|
||||
return self.user_auth_plugin
|
||||
else:
|
||||
return _ContextAuthPlugin(self.auth_token, self.service_catalog)
|
||||
|
||||
def _get_read_deleted(self):
|
||||
return self._read_deleted
|
||||
|
||||
|
|
|
@ -12,11 +12,10 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from cinderclient import exceptions as cinder_exception
|
||||
from cinderclient.v1 import client as cinder_client_v1
|
||||
from cinderclient.v2 import client as cinder_client_v2
|
||||
import mock
|
||||
import six.moves.urllib.parse as urlparse
|
||||
from requests_mock.contrib import fixture
|
||||
from testtools import matchers
|
||||
|
||||
from nova import context
|
||||
from nova import exception
|
||||
|
@ -24,382 +23,171 @@ from nova import test
|
|||
from nova.volume import cinder
|
||||
|
||||
|
||||
def _stub_volume(**kwargs):
|
||||
volume = {
|
||||
'display_name': None,
|
||||
'display_description': None,
|
||||
"attachments": [],
|
||||
"availability_zone": "cinder",
|
||||
"created_at": "2012-09-10T00:00:00.000000",
|
||||
"id": '00000000-0000-0000-0000-000000000000',
|
||||
"metadata": {},
|
||||
"size": 1,
|
||||
"snapshot_id": None,
|
||||
"status": "available",
|
||||
"volume_type": "None",
|
||||
"bootable": "true"
|
||||
}
|
||||
volume.update(kwargs)
|
||||
return volume
|
||||
|
||||
|
||||
def _stub_volume_v2(**kwargs):
|
||||
volume_v2 = {
|
||||
'name': None,
|
||||
'description': None,
|
||||
"attachments": [],
|
||||
"availability_zone": "cinderv2",
|
||||
"created_at": "2013-08-10T00:00:00.000000",
|
||||
"id": '00000000-0000-0000-0000-000000000000',
|
||||
"metadata": {},
|
||||
"size": 1,
|
||||
"snapshot_id": None,
|
||||
"status": "available",
|
||||
"volume_type": "None",
|
||||
"bootable": "true"
|
||||
}
|
||||
volume_v2.update(kwargs)
|
||||
return volume_v2
|
||||
|
||||
|
||||
_image_metadata = {
|
||||
'kernel_id': 'fake',
|
||||
'ramdisk_id': 'fake'
|
||||
}
|
||||
|
||||
|
||||
class FakeHTTPClient(cinder.cinder_client.HTTPClient):
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ['GET', 'DELETE']:
|
||||
assert 'body' not in kwargs
|
||||
elif method == 'PUT':
|
||||
assert 'body' in kwargs
|
||||
|
||||
# Call the method
|
||||
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
|
||||
kwargs.update(args)
|
||||
munged_url = url.rsplit('?', 1)[0]
|
||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
||||
munged_url = munged_url.replace('-', '_')
|
||||
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
|
||||
if not hasattr(self, callback):
|
||||
raise AssertionError('Called unknown API method: %s %s, '
|
||||
'expected fakes method name: %s' %
|
||||
(method, url, callback))
|
||||
|
||||
# Note the call
|
||||
self.callstack.append((method, url, kwargs.get('body', None)))
|
||||
|
||||
status, body = getattr(self, callback)(**kwargs)
|
||||
if hasattr(status, 'items'):
|
||||
return status, body
|
||||
else:
|
||||
return {"status": status}, body
|
||||
|
||||
def get_volumes_1234(self, **kw):
|
||||
volume = {'volume': _stub_volume(id='1234')}
|
||||
return (200, volume)
|
||||
|
||||
def get_volumes_nonexisting(self, **kw):
|
||||
raise cinder_exception.NotFound(code=404, message='Resource not found')
|
||||
|
||||
def get_volumes_5678(self, **kw):
|
||||
"""Volume with image metadata."""
|
||||
volume = {'volume': _stub_volume(id='1234',
|
||||
volume_image_metadata=_image_metadata)
|
||||
}
|
||||
return (200, volume)
|
||||
|
||||
|
||||
class FakeHTTPClientV2(cinder.cinder_client.HTTPClient):
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ['GET', 'DELETE']:
|
||||
assert 'body' not in kwargs
|
||||
elif method == 'PUT':
|
||||
assert 'body' in kwargs
|
||||
|
||||
# Call the method
|
||||
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
|
||||
kwargs.update(args)
|
||||
munged_url = url.rsplit('?', 1)[0]
|
||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
||||
munged_url = munged_url.replace('-', '_')
|
||||
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
|
||||
if not hasattr(self, callback):
|
||||
raise AssertionError('Called unknown API method: %s %s, '
|
||||
'expected fakes method name: %s' %
|
||||
(method, url, callback))
|
||||
|
||||
# Note the call
|
||||
self.callstack.append((method, url, kwargs.get('body', None)))
|
||||
|
||||
status, body = getattr(self, callback)(**kwargs)
|
||||
if hasattr(status, 'items'):
|
||||
return status, body
|
||||
else:
|
||||
return {"status": status}, body
|
||||
|
||||
def get_volumes_1234(self, **kw):
|
||||
volume = {'volume': _stub_volume_v2(id='1234')}
|
||||
return (200, volume)
|
||||
|
||||
def get_volumes_nonexisting(self, **kw):
|
||||
raise cinder_exception.NotFound(code=404, message='Resource not found')
|
||||
|
||||
def get_volumes_5678(self, **kw):
|
||||
"""Volume with image metadata."""
|
||||
volume = {'volume': _stub_volume_v2(
|
||||
id='1234',
|
||||
volume_image_metadata=_image_metadata)
|
||||
}
|
||||
return (200, volume)
|
||||
|
||||
|
||||
class FakeCinderClient(cinder_client_v1.Client):
|
||||
|
||||
def __init__(self, username, password, project_id=None, auth_url=None,
|
||||
insecure=False, retries=None, cacert=None, timeout=None):
|
||||
super(FakeCinderClient, self).__init__(username, password,
|
||||
project_id=project_id,
|
||||
auth_url=auth_url,
|
||||
insecure=insecure,
|
||||
retries=retries,
|
||||
cacert=cacert,
|
||||
timeout=timeout)
|
||||
self.client = FakeHTTPClient(username, password, project_id, auth_url,
|
||||
insecure=insecure, retries=retries,
|
||||
cacert=cacert, timeout=timeout)
|
||||
# keep a ref to the clients callstack for factory's assert_called
|
||||
self.callstack = self.client.callstack = []
|
||||
|
||||
|
||||
class FakeCinderClientV2(cinder_client_v2.Client):
|
||||
|
||||
def __init__(self, username, password, project_id=None, auth_url=None,
|
||||
insecure=False, retries=None, cacert=None, timeout=None):
|
||||
super(FakeCinderClientV2, self).__init__(username, password,
|
||||
project_id=project_id,
|
||||
auth_url=auth_url,
|
||||
insecure=insecure,
|
||||
retries=retries,
|
||||
cacert=cacert,
|
||||
timeout=timeout)
|
||||
self.client = FakeHTTPClientV2(username, password, project_id,
|
||||
auth_url, insecure=insecure,
|
||||
retries=retries, cacert=cacert,
|
||||
timeout=timeout)
|
||||
# keep a ref to the clients callstack for factory's assert_called
|
||||
self.callstack = self.client.callstack = []
|
||||
|
||||
|
||||
class FakeClientFactory(object):
|
||||
"""Keep a ref to the FakeClient since volume.api.cinder throws it away."""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.client = FakeCinderClient(*args, **kwargs)
|
||||
return self.client
|
||||
|
||||
def assert_called(self, method, url, body=None, pos=-1):
|
||||
expected = (method, url)
|
||||
called = self.client.callstack[pos][0:2]
|
||||
|
||||
assert self.client.callstack, ("Expected %s %s but no calls "
|
||||
"were made." % expected)
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % (expected +
|
||||
called)
|
||||
|
||||
if body is not None:
|
||||
assert self.client.callstack[pos][2] == body
|
||||
|
||||
|
||||
class FakeClientV2Factory(object):
|
||||
"""Keep a ref to the FakeClient since volume.api.cinder throws it away."""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.client = FakeCinderClientV2(*args, **kwargs)
|
||||
return self.client
|
||||
|
||||
def assert_called(self, method, url, body=None, pos=-1):
|
||||
expected = (method, url)
|
||||
called = self.client.callstack[pos][0:2]
|
||||
|
||||
assert self.client.callstack, ("Expected %s %s but no calls "
|
||||
"were made." % expected)
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % (expected +
|
||||
called)
|
||||
|
||||
if body is not None:
|
||||
assert self.client.callstack[pos][2] == body
|
||||
|
||||
|
||||
fake_client_factory = FakeClientFactory()
|
||||
fake_client_v2_factory = FakeClientV2Factory()
|
||||
|
||||
|
||||
@mock.patch.object(cinder_client_v1, 'Client', fake_client_factory)
|
||||
class CinderTestCase(test.NoDBTestCase):
|
||||
"""Test case for cinder volume v1 api."""
|
||||
class BaseCinderTestCase(object):
|
||||
|
||||
def setUp(self):
|
||||
super(CinderTestCase, self).setUp()
|
||||
catalog = [{
|
||||
"type": "volume",
|
||||
"name": "cinder",
|
||||
"endpoints": [{"publicURL": "http://localhost:8776/v1/project_id"}]
|
||||
}]
|
||||
cinder.CONF.set_override('catalog_info',
|
||||
'volume:cinder:publicURL', group='cinder')
|
||||
self.context = context.RequestContext('username', 'project_id',
|
||||
service_catalog=catalog)
|
||||
cinder.cinderclient(self.context)
|
||||
|
||||
super(BaseCinderTestCase, self).setUp()
|
||||
cinder.reset_globals()
|
||||
self.requests = self.useFixture(fixture.Fixture())
|
||||
self.api = cinder.API()
|
||||
|
||||
def assert_called(self, *args, **kwargs):
|
||||
fake_client_factory.assert_called(*args, **kwargs)
|
||||
self.context = context.RequestContext('username',
|
||||
'project_id',
|
||||
auth_token='token',
|
||||
service_catalog=self.CATALOG)
|
||||
|
||||
def flags(self, *args, **kwargs):
|
||||
super(BaseCinderTestCase, self).flags(*args, **kwargs)
|
||||
cinder.reset_globals()
|
||||
|
||||
def create_client(self):
|
||||
return cinder.cinderclient(self.context)
|
||||
|
||||
def test_context_with_catalog(self):
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
fake_client_factory.client.client.management_url,
|
||||
'http://localhost:8776/v1/project_id')
|
||||
|
||||
def test_cinder_endpoint_template(self):
|
||||
self.flags(
|
||||
endpoint_template='http://other_host:8776/v1/%(project_id)s',
|
||||
group='cinder'
|
||||
)
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
fake_client_factory.client.client.management_url,
|
||||
'http://other_host:8776/v1/project_id')
|
||||
|
||||
def test_get_non_existing_volume(self):
|
||||
self.assertRaises(exception.VolumeNotFound, self.api.get, self.context,
|
||||
'nonexisting')
|
||||
|
||||
def test_volume_with_image_metadata(self):
|
||||
volume = self.api.get(self.context, '5678')
|
||||
self.assert_called('GET', '/volumes/5678')
|
||||
self.assertIn('volume_image_metadata', volume)
|
||||
self.assertEqual(volume['volume_image_metadata'], _image_metadata)
|
||||
|
||||
def test_cinder_api_insecure(self):
|
||||
# The True/False negation is awkward, but better for the client
|
||||
# to pass us insecure=True and we check verify_cert == False
|
||||
self.flags(api_insecure=True, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
fake_client_factory.client.client.verify_cert, False)
|
||||
|
||||
def test_cinder_api_cacert_file(self):
|
||||
cacert = "/etc/ssl/certs/ca-certificates.crt"
|
||||
self.flags(ca_certificates_file=cacert, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
fake_client_factory.client.client.verify_cert, cacert)
|
||||
self.assertEqual(self.URL, self.create_client().client.get_endpoint())
|
||||
|
||||
def test_cinder_http_retries(self):
|
||||
retries = 42
|
||||
self.flags(http_retries=retries, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
fake_client_factory.client.client.retries, retries)
|
||||
|
||||
|
||||
@mock.patch.object(cinder_client_v2, 'Client', fake_client_v2_factory)
|
||||
class CinderV2TestCase(test.NoDBTestCase):
|
||||
"""Test case for cinder volume v2 api."""
|
||||
|
||||
def setUp(self):
|
||||
super(CinderV2TestCase, self).setUp()
|
||||
catalog = [{
|
||||
"type": "volumev2",
|
||||
"name": "cinderv2",
|
||||
"endpoints": [{"publicURL": "http://localhost:8776/v2/project_id"}]
|
||||
}]
|
||||
self.context = context.RequestContext('username', 'project_id',
|
||||
service_catalog=catalog)
|
||||
|
||||
cinder.cinderclient(self.context)
|
||||
self.api = cinder.API()
|
||||
|
||||
def tearDown(self):
|
||||
cinder.CONF.reset()
|
||||
super(CinderV2TestCase, self).tearDown()
|
||||
|
||||
def assert_called(self, *args, **kwargs):
|
||||
fake_client_v2_factory.assert_called(*args, **kwargs)
|
||||
|
||||
def test_context_with_catalog(self):
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
'http://localhost:8776/v2/project_id',
|
||||
fake_client_v2_factory.client.client.management_url)
|
||||
|
||||
def test_cinder_endpoint_template(self):
|
||||
self.flags(
|
||||
endpoint_template='http://other_host:8776/v2/%(project_id)s',
|
||||
group='cinder'
|
||||
)
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(
|
||||
'http://other_host:8776/v2/project_id',
|
||||
fake_client_v2_factory.client.client.management_url)
|
||||
|
||||
def test_get_non_existing_volume(self):
|
||||
self.assertRaises(exception.VolumeNotFound, self.api.get, self.context,
|
||||
'nonexisting')
|
||||
|
||||
def test_volume_with_image_metadata(self):
|
||||
volume = self.api.get(self.context, '5678')
|
||||
self.assert_called('GET', '/volumes/5678')
|
||||
self.assertIn('volume_image_metadata', volume)
|
||||
self.assertEqual(_image_metadata, volume['volume_image_metadata'])
|
||||
self.assertEqual(retries, self.create_client().client.connect_retries)
|
||||
|
||||
def test_cinder_api_insecure(self):
|
||||
# The True/False negation is awkward, but better for the client
|
||||
# to pass us insecure=True and we check verify_cert == False
|
||||
self.flags(api_insecure=True, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertFalse(fake_client_v2_factory.client.client.verify_cert)
|
||||
|
||||
def test_cinder_api_cacert_file(self):
|
||||
cacert = "/etc/ssl/certs/ca-certificates.crt"
|
||||
self.flags(ca_certificates_file=cacert, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(cacert,
|
||||
fake_client_v2_factory.client.client.verify_cert)
|
||||
|
||||
def test_cinder_http_retries(self):
|
||||
retries = 42
|
||||
self.flags(http_retries=retries, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assert_called('GET', '/volumes/1234')
|
||||
self.assertEqual(retries, fake_client_v2_factory.client.client.retries)
|
||||
self.flags(insecure=True, group='cinder')
|
||||
self.assertFalse(self.create_client().client.session.verify)
|
||||
|
||||
def test_cinder_http_timeout(self):
|
||||
timeout = 123
|
||||
self.flags(http_timeout=timeout, group='cinder')
|
||||
self.api.get(self.context, '1234')
|
||||
self.assertEqual(timeout,
|
||||
fake_client_v2_factory.client.client.timeout)
|
||||
self.flags(timeout=timeout, group='cinder')
|
||||
self.assertEqual(timeout, self.create_client().client.session.timeout)
|
||||
|
||||
def test_cinder_api_cacert_file(self):
|
||||
cacert = "/etc/ssl/certs/ca-certificates.crt"
|
||||
self.flags(cafile=cacert, group='cinder')
|
||||
self.assertEqual(self.create_client().client.session.verify, cacert)
|
||||
|
||||
|
||||
class CinderTestCase(BaseCinderTestCase, test.NoDBTestCase):
|
||||
"""Test case for cinder volume v1 api."""
|
||||
|
||||
URL = "http://localhost:8776/v1/project_id"
|
||||
|
||||
CATALOG = [{
|
||||
"type": "volumev2",
|
||||
"name": "cinderv2",
|
||||
"endpoints": [{"publicURL": URL}]
|
||||
}]
|
||||
|
||||
def create_client(self):
|
||||
c = super(CinderTestCase, self).create_client()
|
||||
self.assertIsInstance(c, cinder_client_v1.Client)
|
||||
return c
|
||||
|
||||
def stub_volume(self, **kwargs):
|
||||
volume = {
|
||||
'display_name': None,
|
||||
'display_description': None,
|
||||
"attachments": [],
|
||||
"availability_zone": "cinder",
|
||||
"created_at": "2012-09-10T00:00:00.000000",
|
||||
"id": '00000000-0000-0000-0000-000000000000',
|
||||
"metadata": {},
|
||||
"size": 1,
|
||||
"snapshot_id": None,
|
||||
"status": "available",
|
||||
"volume_type": "None",
|
||||
"bootable": "true"
|
||||
}
|
||||
volume.update(kwargs)
|
||||
return volume
|
||||
|
||||
def test_cinder_endpoint_template(self):
|
||||
endpoint = 'http://other_host:8776/v1/%(project_id)s'
|
||||
self.flags(endpoint_template=endpoint, group='cinder')
|
||||
self.assertEqual('http://other_host:8776/v1/project_id',
|
||||
self.create_client().client.endpoint_override)
|
||||
|
||||
def test_get_non_existing_volume(self):
|
||||
self.requests.get(self.URL + '/volumes/nonexisting',
|
||||
status_code=404)
|
||||
|
||||
self.assertRaises(exception.VolumeNotFound, self.api.get, self.context,
|
||||
'nonexisting')
|
||||
|
||||
def test_volume_with_image_metadata(self):
|
||||
v = self.stub_volume(id='1234', volume_image_metadata=_image_metadata)
|
||||
m = self.requests.get(self.URL + '/volumes/5678', json={'volume': v})
|
||||
|
||||
volume = self.api.get(self.context, '5678')
|
||||
self.assertThat(m.last_request.path,
|
||||
matchers.EndsWith('/volumes/5678'))
|
||||
self.assertIn('volume_image_metadata', volume)
|
||||
self.assertEqual(volume['volume_image_metadata'], _image_metadata)
|
||||
|
||||
|
||||
class CinderV2TestCase(BaseCinderTestCase, test.NoDBTestCase):
|
||||
"""Test case for cinder volume v2 api."""
|
||||
|
||||
URL = "http://localhost:8776/v2/project_id"
|
||||
|
||||
CATALOG = [{
|
||||
"type": "volumev2",
|
||||
"name": "cinder",
|
||||
"endpoints": [{"publicURL": URL}]
|
||||
}]
|
||||
|
||||
def setUp(self):
|
||||
super(CinderV2TestCase, self).setUp()
|
||||
cinder.CONF.set_override('catalog_info',
|
||||
'volumev2:cinder:publicURL', group='cinder')
|
||||
self.addCleanup(cinder.CONF.reset)
|
||||
|
||||
def create_client(self):
|
||||
c = super(CinderV2TestCase, self).create_client()
|
||||
self.assertIsInstance(c, cinder_client_v2.Client)
|
||||
return c
|
||||
|
||||
def stub_volume(self, **kwargs):
|
||||
volume = {
|
||||
'name': None,
|
||||
'description': None,
|
||||
"attachments": [],
|
||||
"availability_zone": "cinderv2",
|
||||
"created_at": "2013-08-10T00:00:00.000000",
|
||||
"id": '00000000-0000-0000-0000-000000000000',
|
||||
"metadata": {},
|
||||
"size": 1,
|
||||
"snapshot_id": None,
|
||||
"status": "available",
|
||||
"volume_type": "None",
|
||||
"bootable": "true"
|
||||
}
|
||||
volume.update(kwargs)
|
||||
return volume
|
||||
|
||||
def test_cinder_endpoint_template(self):
|
||||
endpoint = 'http://other_host:8776/v2/%(project_id)s'
|
||||
self.flags(endpoint_template=endpoint, group='cinder')
|
||||
self.assertEqual('http://other_host:8776/v2/project_id',
|
||||
self.create_client().client.endpoint_override)
|
||||
|
||||
def test_get_non_existing_volume(self):
|
||||
self.requests.get(self.URL + '/volumes/nonexisting',
|
||||
status_code=404)
|
||||
|
||||
self.assertRaises(exception.VolumeNotFound, self.api.get, self.context,
|
||||
'nonexisting')
|
||||
|
||||
def test_volume_with_image_metadata(self):
|
||||
v = self.stub_volume(id='1234', volume_image_metadata=_image_metadata)
|
||||
self.requests.get(self.URL + '/volumes/5678', json={'volume': v})
|
||||
volume = self.api.get(self.context, '5678')
|
||||
self.assertIn('volume_image_metadata', volume)
|
||||
self.assertEqual(_image_metadata, volume['volume_image_metadata'])
|
||||
|
|
|
@ -93,26 +93,22 @@ class CinderApiTestCase(test.NoDBTestCase):
|
|||
self.api.get, self.ctx, volume_id)
|
||||
|
||||
def test_create(self):
|
||||
cinder.get_cinder_client_version(self.ctx).AndReturn('2')
|
||||
cinder.cinderclient(self.ctx).AndReturn(self.cinderclient)
|
||||
cinder._untranslate_volume_summary_view(self.ctx, {'id': 'created_id'})
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.api.create(self.ctx, 1, '', '')
|
||||
|
||||
def test_create_failed(self):
|
||||
cinder.get_cinder_client_version(self.ctx).AndReturn('2')
|
||||
cinder.cinderclient(self.ctx).AndRaise(cinder_exception.BadRequest(''))
|
||||
self.mox.ReplayAll()
|
||||
@mock.patch('nova.volume.cinder.cinderclient')
|
||||
def test_create_failed(self, mock_cinderclient):
|
||||
mock_cinderclient.return_value.volumes.create.side_effect = (
|
||||
cinder_exception.BadRequest(''))
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.create, self.ctx, 1, '', '')
|
||||
|
||||
@mock.patch('nova.volume.cinder.get_cinder_client_version')
|
||||
@mock.patch('nova.volume.cinder.cinderclient')
|
||||
def test_create_over_quota_failed(self, mock_cinderclient,
|
||||
mock_get_version):
|
||||
mock_get_version.return_value = '2'
|
||||
def test_create_over_quota_failed(self, mock_cinderclient):
|
||||
mock_cinderclient.return_value.volumes.create.side_effect = (
|
||||
cinder_exception.OverLimit(413))
|
||||
self.assertRaises(exception.OverQuota, self.api.create, self.ctx,
|
||||
|
|
|
@ -23,7 +23,9 @@ import sys
|
|||
|
||||
from cinderclient import client as cinder_client
|
||||
from cinderclient import exceptions as cinder_exception
|
||||
from cinderclient import service_catalog
|
||||
from cinderclient.v1 import client as v1_client
|
||||
from keystoneclient import exceptions as keystone_exception
|
||||
from keystoneclient import session
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import strutils
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
@ -45,17 +47,9 @@ cinder_opts = [
|
|||
'endpoint e.g. http://localhost:8776/v1/%(project_id)s'),
|
||||
cfg.StrOpt('os_region_name',
|
||||
help='Region name of this node'),
|
||||
cfg.StrOpt('ca_certificates_file',
|
||||
help='Location of ca certificates file to use for cinder '
|
||||
'client requests.'),
|
||||
cfg.IntOpt('http_retries',
|
||||
default=3,
|
||||
help='Number of cinderclient retries on failed http calls'),
|
||||
cfg.IntOpt('http_timeout',
|
||||
help='HTTP inactivity timeout (in seconds)'),
|
||||
cfg.BoolOpt('api_insecure',
|
||||
default=False,
|
||||
help='Allow to perform insecure SSL requests to cinder'),
|
||||
cfg.BoolOpt('cross_az_attach',
|
||||
default=True,
|
||||
help='Allow attach between instance and volume in different '
|
||||
|
@ -63,30 +57,81 @@ cinder_opts = [
|
|||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(cinder_opts, group='cinder')
|
||||
CINDER_OPT_GROUP = 'cinder'
|
||||
|
||||
# cinder_opts options in the DEFAULT group were deprecated in Juno
|
||||
CONF.register_opts(cinder_opts, group=CINDER_OPT_GROUP)
|
||||
|
||||
|
||||
deprecated = {'timeout': [cfg.DeprecatedOpt('http_timeout',
|
||||
group=CINDER_OPT_GROUP)],
|
||||
'cafile': [cfg.DeprecatedOpt('ca_certificates_file',
|
||||
group=CINDER_OPT_GROUP)],
|
||||
'insecure': [cfg.DeprecatedOpt('api_insecure',
|
||||
group=CINDER_OPT_GROUP)]}
|
||||
|
||||
session.Session.register_conf_options(CONF,
|
||||
CINDER_OPT_GROUP,
|
||||
deprecated_opts=deprecated)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CINDER_URL = None
|
||||
_SESSION = None
|
||||
_V1_ERROR_RAISED = False
|
||||
|
||||
|
||||
def reset_globals():
|
||||
"""Testing method to reset globals.
|
||||
"""
|
||||
global _SESSION
|
||||
_SESSION = None
|
||||
|
||||
|
||||
def cinderclient(context):
|
||||
global CINDER_URL
|
||||
version = get_cinder_client_version(context)
|
||||
c = cinder_client.Client(version,
|
||||
context.user_id,
|
||||
context.auth_token,
|
||||
project_id=context.project_id,
|
||||
auth_url=CINDER_URL,
|
||||
insecure=CONF.cinder.api_insecure,
|
||||
retries=CONF.cinder.http_retries,
|
||||
timeout=CONF.cinder.http_timeout,
|
||||
cacert=CONF.cinder.ca_certificates_file)
|
||||
# noauth extracts user_id:project_id from auth_token
|
||||
c.client.auth_token = context.auth_token or '%s:%s' % (context.user_id,
|
||||
context.project_id)
|
||||
c.client.management_url = CINDER_URL
|
||||
return c
|
||||
global _SESSION
|
||||
global _V1_ERROR_RAISED
|
||||
|
||||
if not _SESSION:
|
||||
_SESSION = session.Session.load_from_conf_options(CONF,
|
||||
CINDER_OPT_GROUP)
|
||||
|
||||
url = None
|
||||
endpoint_override = None
|
||||
version = None
|
||||
|
||||
auth = context.get_auth_plugin()
|
||||
service_type, service_name, interface = CONF.cinder.catalog_info.split(':')
|
||||
|
||||
service_parameters = {'service_type': service_type,
|
||||
'service_name': service_name,
|
||||
'interface': interface,
|
||||
'region_name': CONF.cinder.os_region_name}
|
||||
|
||||
if CONF.cinder.endpoint_template:
|
||||
url = CONF.cinder.endpoint_template % context.to_dict()
|
||||
endpoint_override = url
|
||||
else:
|
||||
url = _SESSION.get_endpoint(auth, **service_parameters)
|
||||
|
||||
# TODO(jamielennox): This should be using proper version discovery from
|
||||
# the cinder service rather than just inspecting the URL for certain string
|
||||
# values.
|
||||
version = get_cinder_client_version(url)
|
||||
|
||||
if version == '1' and not _V1_ERROR_RAISED:
|
||||
msg = _LW('Cinder V1 API is deprecated as of the Juno '
|
||||
'release, and Nova is still configured to use it. '
|
||||
'Enable the V2 API in Cinder and set '
|
||||
'cinder_catalog_info in nova.conf to use it.')
|
||||
LOG.warn(msg)
|
||||
_V1_ERROR_RAISED = True
|
||||
|
||||
return cinder_client.Client(version,
|
||||
session=_SESSION,
|
||||
auth=auth,
|
||||
endpoint_override=endpoint_override,
|
||||
connect_retries=CONF.cinder.http_retries,
|
||||
**service_parameters)
|
||||
|
||||
|
||||
def _untranslate_volume_summary_view(context, vol):
|
||||
|
@ -166,14 +211,18 @@ def translate_volume_exception(method):
|
|||
def wrapper(self, ctx, volume_id, *args, **kwargs):
|
||||
try:
|
||||
res = method(self, ctx, volume_id, *args, **kwargs)
|
||||
except cinder_exception.ClientException:
|
||||
except (cinder_exception.ClientException,
|
||||
keystone_exception.ClientException):
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
if isinstance(exc_value, cinder_exception.NotFound):
|
||||
if isinstance(exc_value, (keystone_exception.NotFound,
|
||||
cinder_exception.NotFound)):
|
||||
exc_value = exception.VolumeNotFound(volume_id=volume_id)
|
||||
elif isinstance(exc_value, cinder_exception.BadRequest):
|
||||
elif isinstance(exc_value, (keystone_exception.BadRequest,
|
||||
cinder_exception.BadRequest)):
|
||||
exc_value = exception.InvalidInput(reason=exc_value.message)
|
||||
raise exc_value, None, exc_trace
|
||||
except cinder_exception.ConnectionError:
|
||||
except (cinder_exception.ConnectionError,
|
||||
keystone_exception.ConnectionError):
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
exc_value = exception.CinderConnectionFailed(
|
||||
reason=exc_value.message)
|
||||
|
@ -189,12 +238,15 @@ def translate_snapshot_exception(method):
|
|||
def wrapper(self, ctx, snapshot_id, *args, **kwargs):
|
||||
try:
|
||||
res = method(self, ctx, snapshot_id, *args, **kwargs)
|
||||
except cinder_exception.ClientException:
|
||||
except (cinder_exception.ClientException,
|
||||
keystone_exception.ClientException):
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
if isinstance(exc_value, cinder_exception.NotFound):
|
||||
if isinstance(exc_value, (keystone_exception.NotFound,
|
||||
cinder_exception.NotFound)):
|
||||
exc_value = exception.SnapshotNotFound(snapshot_id=snapshot_id)
|
||||
raise exc_value, None, exc_trace
|
||||
except cinder_exception.ConnectionError:
|
||||
except (cinder_exception.ConnectionError,
|
||||
keystone_exception.ConnectionError):
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
exc_value = exception.CinderConnectionFailed(
|
||||
reason=exc_value.message)
|
||||
|
@ -203,58 +255,24 @@ def translate_snapshot_exception(method):
|
|||
return wrapper
|
||||
|
||||
|
||||
def get_cinder_client_version(context):
|
||||
def get_cinder_client_version(url):
|
||||
"""Parse cinder client version by endpoint url.
|
||||
|
||||
:param context: Nova auth context.
|
||||
:param url: URL for cinder.
|
||||
:return: str value(1 or 2).
|
||||
"""
|
||||
global CINDER_URL
|
||||
# FIXME: the cinderclient ServiceCatalog object is mis-named.
|
||||
# It actually contains the entire access blob.
|
||||
# Only needed parts of the service catalog are passed in, see
|
||||
# nova/context.py.
|
||||
compat_catalog = {
|
||||
'access': {'serviceCatalog': context.service_catalog or []}
|
||||
}
|
||||
sc = service_catalog.ServiceCatalog(compat_catalog)
|
||||
if CONF.cinder.endpoint_template:
|
||||
url = CONF.cinder.endpoint_template % context.to_dict()
|
||||
else:
|
||||
info = CONF.cinder.catalog_info
|
||||
service_type, service_name, endpoint_type = info.split(':')
|
||||
# extract the region if set in configuration
|
||||
if CONF.cinder.os_region_name:
|
||||
attr = 'region'
|
||||
filter_value = CONF.cinder.os_region_name
|
||||
else:
|
||||
attr = None
|
||||
filter_value = None
|
||||
url = sc.url_for(attr=attr,
|
||||
filter_value=filter_value,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
endpoint_type=endpoint_type)
|
||||
LOG.debug('Cinderclient connection created using URL: %s', url)
|
||||
|
||||
# FIXME(jamielennox): Use cinder_client.get_volume_api_from_url when
|
||||
# bug #1386232 is fixed.
|
||||
valid_versions = ['v1', 'v2']
|
||||
magic_tuple = urlparse.urlsplit(url)
|
||||
scheme, netloc, path, query, frag = magic_tuple
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||
components = path.split("/")
|
||||
|
||||
for version in valid_versions:
|
||||
if version in components[1]:
|
||||
version = version[1:]
|
||||
if version in components:
|
||||
return version[1:]
|
||||
|
||||
if not CINDER_URL and version == '1':
|
||||
msg = _LW('Cinder V1 API is deprecated as of the Juno '
|
||||
'release, and Nova is still configured to use it. '
|
||||
'Enable the V2 API in Cinder and set '
|
||||
'cinder_catalog_info in nova.conf to use it.')
|
||||
LOG.warn(msg)
|
||||
|
||||
CINDER_URL = url
|
||||
return version
|
||||
msg = _("Invalid client version, must be one of: %s") % valid_versions
|
||||
msg = "Invalid client version '%s'. must be one of: %s" % (
|
||||
(version, ', '.join(valid_versions)))
|
||||
raise cinder_exception.UnsupportedVersion(msg)
|
||||
|
||||
|
||||
|
@ -360,6 +378,7 @@ class API(object):
|
|||
def create(self, context, size, name, description, snapshot=None,
|
||||
image_id=None, volume_type=None, metadata=None,
|
||||
availability_zone=None):
|
||||
client = cinderclient(context)
|
||||
|
||||
if snapshot is not None:
|
||||
snapshot_id = snapshot['id']
|
||||
|
@ -374,20 +393,20 @@ class API(object):
|
|||
metadata=metadata,
|
||||
imageRef=image_id)
|
||||
|
||||
version = get_cinder_client_version(context)
|
||||
if version == '1':
|
||||
if isinstance(client, v1_client.Client):
|
||||
kwargs['display_name'] = name
|
||||
kwargs['display_description'] = description
|
||||
elif version == '2':
|
||||
else:
|
||||
kwargs['name'] = name
|
||||
kwargs['description'] = description
|
||||
|
||||
try:
|
||||
item = cinderclient(context).volumes.create(size, **kwargs)
|
||||
item = client.volumes.create(size, **kwargs)
|
||||
return _untranslate_volume_summary_view(context, item)
|
||||
except cinder_exception.OverLimit:
|
||||
raise exception.OverQuota(overs='volumes')
|
||||
except cinder_exception.BadRequest as e:
|
||||
except (cinder_exception.BadRequest,
|
||||
keystone_exception.BadRequest) as e:
|
||||
raise exception.InvalidInput(reason=e)
|
||||
|
||||
@translate_volume_exception
|
||||
|
|
|
@ -15,6 +15,7 @@ psycopg2
|
|||
pylint>=1.3.0 # GNU GPL v2
|
||||
python-ironicclient>=0.2.1
|
||||
python-subunit>=0.0.18
|
||||
requests-mock>=0.5.1 # Apache-2.0
|
||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||
oslosphinx>=2.2.0 # Apache-2.0
|
||||
oslotest>=1.2.0 # Apache-2.0
|
||||
|
|
Loading…
Reference in New Issue