Use session in cinderclient
Use the common session loading parameters and the session object for talking to cinder. There are some related changes in this patch. Firstly auth_token middleware now provides an authentication plugin that can be used along with the session object to make requests under the user's authentication. This will largely replace the information required on the context object. This authentication plugin is not serializable though and so it cannot be transferred over RPC so we introduce a simple authentication plugin that reconstructs the required information from the context. When talking to cinder we now create a global session object (think of this like keeping open a connection pool object) and use the authentication plugin to send requests to cinder. I also condense the cinder tests as they are largely copied and pasted between v1 and v2 and this solves fixing them in two places. DocImpact: Renames cinder's timeout, insecure and CA certificates parameters to the parameters used by the common session object. Adds options for using client certificates with connection. Change-Id: I7afe604503b8597c16be61d2a66a10b94269a219
This commit is contained in:
parent
a949f64073
commit
4919269542
@ -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
|
||||
@ -2404,7 +2405,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)
|
||||
|
||||
|
||||
@ -350,6 +368,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']
|
||||
@ -364,20 +383,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
Block a user