Support Cinder API version 2

Add support for the second version of the Cinder API, which brings
useful features such as scheduler hints, more consistent responses,
caching, filtering, etc.

Available volume services (such as 'volume' and 'volumev2') are
discovered at run time when creating the Cinder client.  Consequently,
there is not need for deployers to make a choice between API version 1
or 2, because the newest service is automatically chosen.

Implements: blueprint support-cinder-api-v2
Change-Id: If9eedd446e017a2f2a4e1b3257ff0d04834f9603
This commit is contained in:
Adrien Vergé 2014-09-23 12:29:10 +02:00
parent ec0a6720ab
commit ac0e68e865
5 changed files with 135 additions and 56 deletions

View File

@ -11,22 +11,57 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
from cinderclient import client as cc
from cinderclient import exceptions
from heat.engine.clients import client_plugin
from heat.common import exception
from heat.engine import clients
from heat.openstack.common.gettextutils import _
class CinderClientPlugin(client_plugin.ClientPlugin):
LOG = logging.getLogger(__name__)
class CinderClientPlugin(clients.client_plugin.ClientPlugin):
exceptions_module = exceptions
def get_volume_api_version(self):
'''Returns the most recent API version.'''
endpoint_type = self._get_client_option('cinder', 'endpoint_type')
try:
self.url_for(service_type='volumev2', endpoint_type=endpoint_type)
return 2
except exceptions.EndpointNotFound:
try:
self.url_for(service_type='volume',
endpoint_type=endpoint_type)
return 1
except exceptions.EndpointNotFound:
return None
def _create(self):
con = self.context
volume_api_version = self.get_volume_api_version()
if volume_api_version == 1:
service_type = 'volume'
client_version = '1'
elif volume_api_version == 2:
service_type = 'volumev2'
client_version = '2'
else:
raise exception.Error(_('No volume service available.'))
LOG.info(_('Creating Cinder client with volume API version %d.'),
volume_api_version)
endpoint_type = self._get_client_option('cinder', 'endpoint_type')
args = {
'service_type': 'volume',
'service_type': service_type,
'auth_url': con.auth_url or '',
'project_id': con.tenant,
'username': None,
@ -38,12 +73,14 @@ class CinderClientPlugin(client_plugin.ClientPlugin):
'insecure': self._get_client_option('cinder', 'insecure')
}
client = cc.Client('1', **args)
management_url = self.url_for(service_type='volume',
client = cc.Client(client_version, **args)
management_url = self.url_for(service_type=service_type,
endpoint_type=endpoint_type)
client.client.auth_token = self.auth_token
client.client.management_url = management_url
client.volume_api_version = volume_api_version
return client
def is_not_found(self, ex):

View File

@ -84,10 +84,10 @@ class Volume(resource.Resource):
default_client_name = 'cinder'
def _display_name(self):
def _name(self):
return self.physical_resource_name()
def _display_description(self):
def _description(self):
return self.physical_resource_name()
def _create_arguments(self):
@ -111,14 +111,22 @@ class Volume(resource.Resource):
vol_id = cinder.restores.restore(backup_id).volume_id
vol = cinder.volumes.get(vol_id)
vol.update(
display_name=self._display_name(),
display_description=self._display_description())
if cinder.volume_api_version == 1:
kwargs = {'display_name': self._name(),
'display_description': self._description()}
else:
kwargs = {'name': self._name(),
'description': self._description()}
vol.update(**kwargs)
else:
vol = cinder.volumes.create(
display_name=self._display_name(),
display_description=self._display_description(),
**self._create_arguments())
kwargs = self._create_arguments()
if cinder.volume_api_version == 1:
kwargs['display_name'] = self._name()
kwargs['display_description'] = self._description()
else:
kwargs['name'] = self._name()
kwargs['description'] = self._description()
vol = cinder.volumes.create(**kwargs)
self.resource_id_set(vol.id)
return vol
@ -455,8 +463,8 @@ class CinderVolume(Volume):
)
ATTRIBUTES = (
AVAILABILITY_ZONE_ATTR, SIZE_ATTR, SNAPSHOT_ID_ATTR, DISPLAY_NAME,
DISPLAY_DESCRIPTION, VOLUME_TYPE_ATTR, METADATA_ATTR,
AVAILABILITY_ZONE_ATTR, SIZE_ATTR, SNAPSHOT_ID_ATTR, DISPLAY_NAME_ATTR,
DISPLAY_DESCRIPTION_ATTR, VOLUME_TYPE_ATTR, METADATA_ATTR,
SOURCE_VOLID_ATTR, STATUS, CREATED_AT, BOOTABLE, METADATA_VALUES_ATTR,
ENCRYPTED_ATTR, ATTACHMENTS,
) = (
@ -539,10 +547,10 @@ class CinderVolume(Volume):
SNAPSHOT_ID_ATTR: attributes.Schema(
_('The snapshot the volume was created from, if any.')
),
DISPLAY_NAME: attributes.Schema(
DISPLAY_NAME_ATTR: attributes.Schema(
_('Name of the volume.')
),
DISPLAY_DESCRIPTION: attributes.Schema(
DISPLAY_DESCRIPTION_ATTR: attributes.Schema(
_('Description of the volume.')
),
VOLUME_TYPE_ATTR: attributes.Schema(
@ -576,13 +584,13 @@ class CinderVolume(Volume):
_volume_creating_status = ['creating', 'restoring-backup', 'downloading']
def _display_name(self):
def _name(self):
name = self.properties[self.NAME]
if name:
return name
return super(CinderVolume, self)._display_name()
return super(CinderVolume, self)._name()
def _display_description(self):
def _description(self):
return self.properties[self.DESCRIPTION]
def _create_arguments(self):
@ -608,6 +616,11 @@ class CinderVolume(Volume):
return unicode(json.dumps(vol.metadata))
elif name == self.METADATA_VALUES_ATTR:
return vol.metadata
if self.cinder().volume_api_version >= 2:
if name == 'display_name':
return vol.name
elif name == 'display_description':
return vol.description
return unicode(getattr(vol, name))
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
@ -621,8 +634,12 @@ class CinderVolume(Volume):
self.properties.get(self.NAME))
update_description = (prop_diff.get(self.DESCRIPTION) or
self.properties.get(self.DESCRIPTION))
kwargs['display_name'] = update_name
kwargs['display_description'] = update_description
if self.cinder().volume_api_version == 1:
kwargs['display_name'] = update_name
kwargs['display_description'] = update_description
else:
kwargs['name'] = update_name
kwargs['description'] = update_description
self.cinder().volumes.update(vol, **kwargs)
# update the metadata for cinder volume
if self.METADATA in prop_diff:

View File

@ -19,6 +19,8 @@ wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
from cinderclient import exceptions as cinder_exceptions
from heat.common import context
@ -78,13 +80,15 @@ class FakeClient(object):
class FakeKeystoneClient(object):
def __init__(self, username='test_user', password='apassword',
user_id='1234', access='4567', secret='8901',
credential_id='abcdxyz', auth_token='abcd1234'):
credential_id='abcdxyz', auth_token='abcd1234',
only_services=None):
self.username = username
self.password = password
self.user_id = user_id
self.access = access
self.secret = secret
self.credential_id = credential_id
self.only_services = only_services
class FakeCred(object):
id = self.credential_id
@ -128,6 +132,10 @@ class FakeKeystoneClient(object):
pass
def url_for(self, **kwargs):
if self.only_services is not None:
if 'service_type' in kwargs and \
kwargs['service_type'] not in self.only_services:
raise cinder_exceptions.EndpointNotFound()
return 'http://example.com:1234/v1'
def create_trust_context(self):

View File

@ -29,6 +29,7 @@ from testtools.testcase import skip
from heat.engine import clients
from heat.engine.clients import client_plugin
from heat.tests.common import HeatTestCase
from heat.tests import utils
from heat.tests.v1_1 import fakes
@ -527,3 +528,24 @@ class TestIsNotFound(HeatTestCase):
iue = self.is_unprocessable_entity
if iue != client_plugin.is_unprocessable_entity(e):
raise
class ClientAPIVersionTest(HeatTestCase):
def test_cinder_api_v1_and_v2(self):
self.stub_keystoneclient()
ctx = utils.dummy_context()
client = clients.Clients(ctx).client('cinder')
self.assertEqual(2, client.volume_api_version)
def test_cinder_api_v1_only(self):
self.stub_keystoneclient(only_services=['volume'])
ctx = utils.dummy_context()
client = clients.Clients(ctx).client('cinder')
self.assertEqual(1, client.volume_api_version)
def test_cinder_api_v2_only(self):
self.stub_keystoneclient(only_services=['volumev2'])
ctx = utils.dummy_context()
client = clients.Clients(ctx).client('cinder')
self.assertEqual(2, client.volume_api_version)

View File

@ -15,7 +15,7 @@ import copy
import json
from cinderclient import exceptions as cinder_exp
from cinderclient.v1 import client as cinderclient
from cinderclient.v2 import client as cinderclient
import mox
from oslo.config import cfg
import six
@ -103,6 +103,7 @@ class BaseVolumeTest(HeatTestCase):
super(BaseVolumeTest, self).setUp()
self.fc = fakes.FakeClient()
self.cinder_fc = cinderclient.Client('username', 'password')
self.cinder_fc.volume_api_version = 2
self.m.StubOutWithMock(cinder.CinderClientPlugin, '_create')
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
self.m.StubOutWithMock(self.cinder_fc.volumes, 'create')
@ -178,8 +179,8 @@ class VolumeTest(BaseVolumeTest):
vol_name = utils.PhysName(stack_name, 'DataVolume')
self.cinder_fc.volumes.create(
size=1, availability_zone='nova',
display_description=vol_name,
display_name=vol_name,
description=vol_name,
name=vol_name,
metadata={u'Usage': u'Wiki Data Volume'}).AndReturn(fv)
def test_volume(self):
@ -233,8 +234,8 @@ class VolumeTest(BaseVolumeTest):
vol_name = utils.PhysName(stack_name, 'DataVolume')
self.cinder_fc.volumes.create(
size=1, availability_zone=None,
display_description=vol_name,
display_name=vol_name,
description=vol_name,
name=vol_name,
metadata={u'Usage': u'Wiki Data Volume'}).AndReturn(fv)
vol.VolumeAttachment.handle_create().AndReturn(None)
vol.VolumeAttachment.check_create_complete(None).AndReturn(True)
@ -609,9 +610,7 @@ class VolumeTest(BaseVolumeTest):
self.cinder_fc.volumes.get('vol-123').AndReturn(fv)
self.m.StubOutWithMock(fv, 'update')
vol_name = utils.PhysName(stack_name, 'DataVolume')
fv.update(
display_description=vol_name,
display_name=vol_name)
fv.update(description=vol_name, name=vol_name)
self.m.ReplayAll()
@ -638,9 +637,7 @@ class VolumeTest(BaseVolumeTest):
self.cinder_fc.volumes.get('vol-123').AndReturn(fv)
self.m.StubOutWithMock(fv, 'update')
vol_name = utils.PhysName(stack_name, 'DataVolume')
fv.update(
display_description=vol_name,
display_name=vol_name)
fv.update(description=vol_name, name=vol_name)
self.m.ReplayAll()
@ -708,8 +705,8 @@ class CinderVolumeTest(BaseVolumeTest):
self.cinder_fc)
self.cinder_fc.volumes.create(
size=size, availability_zone='nova',
display_description='test_description',
display_name='test_name',
description='test_description',
name='test_name',
metadata={'key': 'value'}).AndReturn(fv)
def test_cinder_volume_size_constraint(self):
@ -730,8 +727,8 @@ class CinderVolumeTest(BaseVolumeTest):
self.cinder_fc)
self.cinder_fc.volumes.create(
size=1, availability_zone='nova',
display_description='test_description',
display_name='test_name',
description='test_description',
name='test_name',
imageRef='46988116-6703-4623-9dbc-2bc6d284021b',
snapshot_id='snap-123',
metadata={'key': 'value'},
@ -767,8 +764,8 @@ class CinderVolumeTest(BaseVolumeTest):
self.cinder_fc.volumes.create(
size=1, availability_zone='nova',
display_description='ImageVolumeDescription',
display_name='ImageVolume',
description='ImageVolumeDescription',
name='ImageVolume',
imageRef=image_id).AndReturn(fv)
self.m.ReplayAll()
@ -795,8 +792,8 @@ class CinderVolumeTest(BaseVolumeTest):
vol_name = utils.PhysName(stack_name, 'volume')
self.cinder_fc.volumes.create(
size=1, availability_zone='nova',
display_description=None,
display_name=vol_name).AndReturn(fv)
description=None,
name=vol_name).AndReturn(fv)
self.m.ReplayAll()
@ -812,8 +809,8 @@ class CinderVolumeTest(BaseVolumeTest):
def test_cinder_fn_getatt(self):
fv = FakeVolume('creating', 'available', availability_zone='zone1',
size=1, snapshot_id='snap-123', display_name='name',
display_description='desc', volume_type='lvm',
size=1, snapshot_id='snap-123', name='name',
description='desc', volume_type='lvm',
metadata={'key': 'value'}, source_volid=None,
status='available', bootable=False,
created_at='2013-02-25T02:40:21.000000',
@ -1081,9 +1078,7 @@ class CinderVolumeTest(BaseVolumeTest):
self.cinder_fc.volumes.get('vol-123').AndReturn(fv)
self.m.StubOutWithMock(fv, 'update')
vol_name = utils.PhysName(stack_name, 'volume')
fv.update(
display_description=None,
display_name=vol_name)
fv.update(description=None, name=vol_name)
# update script
self.cinder_fc.volumes.get(fv.id).AndReturn(fv)
@ -1118,8 +1113,8 @@ class CinderVolumeTest(BaseVolumeTest):
meta = {'Key': 'New Value'}
update_description = 'update_description'
kwargs = {
'display_name': update_name,
'display_description': update_description
'name': update_name,
'description': update_description
}
self._mock_create_volume(fv, stack_name)
@ -1149,8 +1144,8 @@ class CinderVolumeTest(BaseVolumeTest):
cinder.CinderClientPlugin._create().MultipleTimes().AndReturn(
self.cinder_fc)
self.cinder_fc.volumes.create(
size=1, availability_zone='nova', display_name='CustomName',
display_description='CustomDescription').AndReturn(fv)
size=1, availability_zone='nova', name='CustomName',
description='CustomDescription').AndReturn(fv)
self.m.StubOutWithMock(self.cinder_fc.backups, 'create')
self.cinder_fc.backups.create('vol-123').AndReturn(fb)
@ -1189,8 +1184,8 @@ class CinderVolumeTest(BaseVolumeTest):
cinder.CinderClientPlugin._create().MultipleTimes().AndReturn(
self.cinder_fc)
self.cinder_fc.volumes.create(
size=1, availability_zone='nova', display_name='CustomName',
display_description='CustomDescription').AndReturn(fv)
size=1, availability_zone='nova', name='CustomName',
description='CustomDescription').AndReturn(fv)
self.m.StubOutWithMock(self.cinder_fc.backups, 'create')
self.cinder_fc.backups.create('vol-123').AndReturn(fb)
@ -1280,8 +1275,8 @@ class CinderVolumeTest(BaseVolumeTest):
vol2_name = utils.PhysName(stack_name, 'volume2')
self.cinder_fc.volumes.create(
size=2, availability_zone='nova',
display_description=None,
display_name=vol2_name).AndReturn(fv2)
description=None,
name=vol2_name).AndReturn(fv2)
self._mock_create_server_volume_script(fva)