diff --git a/nova/api/auth.py b/nova/api/auth.py index 1c99037570..4c44773057 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -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 diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 12baeadd1d..6350825cbe 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -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) diff --git a/nova/context.py b/nova/context.py index dafc95a6bd..b528231cf6 100644 --- a/nova/context.py +++ b/nova/context.py @@ -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 diff --git a/nova/tests/unit/test_cinder.py b/nova/tests/unit/test_cinder.py index 913b4e4de7..1240b9547a 100644 --- a/nova/tests/unit/test_cinder.py +++ b/nova/tests/unit/test_cinder.py @@ -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']) diff --git a/nova/tests/unit/volume/test_cinder.py b/nova/tests/unit/volume/test_cinder.py index 1aa3f85c97..cf54c9c45c 100644 --- a/nova/tests/unit/volume/test_cinder.py +++ b/nova/tests/unit/volume/test_cinder.py @@ -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, diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index c080c515c0..f2f237f49a 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index 8b754d2bea..0dcee61556 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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