class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase):
+ CONSIS_GROUP_ID = '3470cc4c-63b3-4c7a-8120-8a0693b45838'
+ CGSNAPSHOT_ID = '5351d914-6c90-43e7-9a8e-7e84610927da'
+
+ cgsnapshot = {'consistencygroup_id': CONSIS_GROUP_ID,
+ 'description': 'cgsnapshot',
+ 'id': CGSNAPSHOT_ID,
+ 'readOnly': False}
+
driver_startup_call_stack = [
mock.call.login('foo1', 'bar2'),
mock.call.getClusterByName('CloudCluster1'),
_mock_client.return_value.getCluster.return_value = {
'spaceTotal': units.Gi * 500,
'spaceAvailable': units.Gi * 250}
+ db = mock.Mock()
self.driver = hp_lefthand_iscsi.HPLeftHandISCSIDriver(
- configuration=config)
+ configuration=config, db=db)
self.driver.do_setup(None)
self.cluster_name = config.hplefthand_clustername
return _mock_client.return_value
mock.call.logout()]
mock_client.assert_has_calls(expected)
+
+ @mock.patch('hplefthandclient.version', "1.0.6")
+ def test_create_consistencygroup(self):
+ class fake_consitencygroup_object(object):
+ volume_type_id = '371c64d5-b92a-488c-bc14-1e63cef40e08'
+ name = 'cg_name'
+ cgsnapshot_id = None
+ id = self.CONSIS_GROUP_ID
+ description = 'consistency group'
+
+ ctxt = context.get_admin_context()
+ # set up driver with default config
+ mock_client = self.setup_driver()
+
+ with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ '_create_client') as mock_do_setup:
+ mock_do_setup.return_value = mock_client
+
+ # create a consistency group
+ group = fake_consitencygroup_object()
+ cg = self.driver.create_consistencygroup(ctxt, group)
+
+ self.assertEqual('available', cg['status'])
+
+ @mock.patch('hplefthandclient.version', "1.0.6")
+ def test_delete_consistencygroup(self):
+ class fake_consitencygroup_object(object):
+ volume_type_id = '371c64d5-b92a-488c-bc14-1e63cef40e08'
+ name = 'cg_name'
+ cgsnapshot_id = None
+ id = self.CONSIS_GROUP_ID
+ description = 'consistency group'
+
+ ctxt = context.get_admin_context()
+ # set up driver with default config
+ mock_client = self.setup_driver()
+
+ mock_volume = mock.MagicMock()
+ expected_volumes = [mock_volume]
+ self.driver.db.volume_get_all_by_group.return_value = expected_volumes
+
+ with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ '_create_client') as mock_do_setup:
+ mock_do_setup.return_value = mock_client
+
+ # create a consistency group
+ group = fake_consitencygroup_object()
+ cg = self.driver.create_consistencygroup(ctxt, group)
+ self.assertEqual('available', cg['status'])
+
+ # delete the consistency group
+ group.status = 'deleting'
+ cg, vols = self.driver.delete_consistencygroup(ctxt, group)
+ self.assertEqual('deleting', cg['status'])
+
+ @mock.patch('hplefthandclient.version', "1.0.6")
+ def test_update_consistencygroup_add_vol_delete_cg(self):
+ class fake_consitencygroup_object(object):
+ volume_type_id = '371c64d5-b92a-488c-bc14-1e63cef40e08'
+ name = 'cg_name'
+ cgsnapshot_id = None
+ id = self.CONSIS_GROUP_ID
+ description = 'consistency group'
+
+ ctxt = context.get_admin_context()
+
+ # set up driver with default config
+ mock_client = self.setup_driver()
+
+ mock_volume = mock.MagicMock()
+ expected_volumes = [mock_volume]
+ self.driver.db.volume_get_all_by_group.return_value = expected_volumes
+
+ mock_client.getVolumes.return_value = {'total': 1, 'members': []}
+
+ # mock return value of createVolume
+ mock_client.createVolume.return_value = {
+ 'iscsiIqn': self.connector['initiator']}
+
+ with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ '_create_client') as mock_do_setup:
+ mock_do_setup.return_value = mock_client
+
+ # create a consistency group
+ group = fake_consitencygroup_object()
+ cg = self.driver.create_consistencygroup(ctxt, group)
+ self.assertEqual('available', cg['status'])
+
+ # add volume to consistency group
+ cg = self.driver.update_consistencygroup(
+ ctxt, group, add_volumes=[self.volume], remove_volumes=None)
+
+ # delete the consistency group
+ group.status = 'deleting'
+ cg, vols = self.driver.delete_consistencygroup(ctxt, group)
+ self.assertEqual('deleting', cg['status'])
+
+ @mock.patch('hplefthandclient.version', "1.0.6")
+ def test_update_consistencygroup_remove_vol_delete_cg(self):
+ class fake_consitencygroup_object(object):
+ volume_type_id = '371c64d5-b92a-488c-bc14-1e63cef40e08'
+ name = 'cg_name'
+ cgsnapshot_id = None
+ id = self.CONSIS_GROUP_ID
+ description = 'consistency group'
+
+ ctxt = context.get_admin_context()
+
+ # set up driver with default config
+ mock_client = self.setup_driver()
+
+ mock_volume = mock.MagicMock()
+ expected_volumes = [mock_volume]
+ self.driver.db.volume_get_all_by_group.return_value = expected_volumes
+
+ mock_client.getVolumes.return_value = {'total': 1, 'members': []}
+
+ # mock return value of createVolume
+ mock_client.createVolume.return_value = {
+ 'iscsiIqn': self.connector['initiator']}
+
+ with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ '_create_client') as mock_do_setup:
+ mock_do_setup.return_value = mock_client
+
+ # create a consistency group
+ group = fake_consitencygroup_object()
+ cg = self.driver.create_consistencygroup(ctxt, group)
+ self.assertEqual('available', cg['status'])
+
+ # add volume to consistency group
+ cg = self.driver.update_consistencygroup(
+ ctxt, group, add_volumes=[self.volume], remove_volumes=None)
+
+ # remove volume from consistency group
+ cg = self.driver.update_consistencygroup(
+ ctxt, group, add_volumes=None, remove_volumes=[self.volume])
+
+ # delete the consistency group
+ group.status = 'deleting'
+ cg, vols = self.driver.delete_consistencygroup(ctxt, group)
+ self.assertEqual('deleting', cg['status'])
+
+ @mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_cgsnapshot')
+ @mock.patch('hplefthandclient.version', "1.0.6")
+ def test_create_cgsnapshot(self, mock_snap_list):
+ class fake_consitencygroup_object(object):
+ volume_type_id = '371c64d5-b92a-488c-bc14-1e63cef40e08'
+ name = 'cg_name'
+ cgsnapshot_id = None
+ id = self.CONSIS_GROUP_ID
+ description = 'consistency group'
+
+ ctxt = context.get_admin_context()
+
+ # set up driver with default config
+ mock_client = self.setup_driver()
+
+ mock_client.getVolumes.return_value = {'total': 1, 'members': []}
+ mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+ mock_snap = mock.MagicMock()
+ mock_snap.volumeName = self.volume_name
+ expected_snaps = [mock_snap]
+ mock_snap_list.return_value = expected_snaps
+
+ with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ '_create_client') as mock_do_setup:
+ mock_do_setup.return_value = mock_client
+
+ # create a consistency group
+ group = fake_consitencygroup_object()
+ cg = self.driver.create_consistencygroup(ctxt, group)
+ self.assertEqual('available', cg['status'])
+
+ # create volume and add it to the consistency group
+ self.driver.update_consistencygroup(
+ ctxt, group, add_volumes=[self.volume], remove_volumes=None)
+
+ # create the conistency group snapshot
+ cgsnap, snaps = self.driver.create_cgsnapshot(
+ ctxt, self.cgsnapshot)
+ self.assertEqual('available', cgsnap['status'])
+
+ @mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_cgsnapshot')
+ @mock.patch('hplefthandclient.version', "1.0.6")
+ def test_delete_cgsnapshot(self, mock_snap_list):
+ class fake_consitencygroup_object(object):
+ volume_type_id = '371c64d5-b92a-488c-bc14-1e63cef40e08'
+ name = 'cg_name'
+ cgsnapshot_id = None
+ id = self.CONSIS_GROUP_ID
+ description = 'consistency group'
+
+ ctxt = context.get_admin_context()
+
+ # set up driver with default config
+ mock_client = self.setup_driver()
+
+ mock_client.getVolumes.return_value = {'total': 1, 'members': []}
+ mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+ mock_snap = mock.MagicMock()
+ mock_snap.volumeName = self.volume_name
+ expected_snaps = [mock_snap]
+ mock_snap_list.return_value = expected_snaps
+
+ with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ '_create_client') as mock_do_setup:
+ mock_do_setup.return_value = mock_client
+
+ # create a consistency group
+ group = fake_consitencygroup_object()
+ cg = self.driver.create_consistencygroup(ctxt, group)
+ self.assertEqual('available', cg['status'])
+
+ # create volume and add it to the consistency group
+ self.driver.update_consistencygroup(
+ ctxt, group, add_volumes=[self.volume], remove_volumes=None)
+
+ # delete the consistency group snapshot
+ self.cgsnapshot['status'] = 'deleting'
+ cgsnap, snaps = self.driver.delete_cgsnapshot(
+ ctxt, self.cgsnapshot)
+ self.assertEqual('deleting', cgsnap['status'])
driver.CloneableVD,
driver.SnapshotVD,
driver.MigrateVD,
- driver.BaseVD):
+ driver.BaseVD,
+ driver.ConsistencyGroupVD):
"""Executes commands relating to HP/LeftHand SAN ISCSI volumes.
Version history:
1.0.5 - Adding support for manage/unmanage.
1.0.6 - Updated minimum client version. bug #1432757
1.0.7 - Update driver to use ABC metaclasses
+ 1.0.8 - Adds consistency group support
"""
- VERSION = "1.0.7"
+ VERSION = "1.0.8"
def __init__(self, *args, **kwargs):
super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs)
"""Extend the size of an existing volume."""
self.proxy.extend_volume(volume, new_size)
+ def create_consistencygroup(self, context, group):
+ """Creates a consistency group."""
+ return self.proxy.create_consistencygroup(context, group)
+
+ def create_consistencygroup_from_src(self, context, group, volumes,
+ cgsnapshot=None, snapshots=None,
+ source_cg=None, source_vols=None):
+ """Creates a consistency group from a source"""
+ return self.proxy.create_consistencygroup_from_src(
+ context, group, volumes, cgsnapshot, snapshots, source_cg,
+ source_vols)
+
+ def delete_consistencygroup(self, context, group):
+ """Deletes a consistency group."""
+ return self.proxy.delete_consistencygroup(context, group)
+
+ def update_consistencygroup(self, context, group,
+ add_volumes=None, remove_volumes=None):
+ """Updates a consistency group."""
+ return self.proxy.update_consistencygroup(context, group, add_volumes,
+ remove_volumes)
+
+ def create_cgsnapshot(self, context, cgsnapshot):
+ """Creates a consistency group snapshot."""
+ return self.proxy.create_cgsnapshot(context, cgsnapshot)
+
+ def delete_cgsnapshot(self, context, cgsnapshot):
+ """Deletes a consistency group snapshot."""
+ return self.proxy.delete_cgsnapshot(context, cgsnapshot)
+
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
return self.proxy.create_volume_from_snapshot(volume, snapshot)
from cinder import context
from cinder import exception
from cinder.i18n import _, _LE, _LI, _LW
+from cinder import objects
from cinder.volume import driver
from cinder.volume import utils
from cinder.volume import volume_types
CONF.register_opts(hplefthand_opts)
MIN_API_VERSION = "1.1"
+MIN_CG_CLIENT_VERSION = "1.0.6"
# map the extra spec key to the REST client option key
extra_specs_key_map = {
1.0.9 - Adding support for manage/unmanage.
1.0.10 - Add stats for goodness_function and filter_function
1.0.11 - Add over subscription support
+ 1.0.12 - Adds consistency group support
"""
- VERSION = "1.0.11"
+ VERSION = "1.0.12"
device_stats = {}
# blank is the only invalid character for cluster names
# so we need to use it as a separator
self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s'
+ self.db = kwargs.get('db')
def _login(self):
client = self.do_setup(None)
finally:
self._logout(client)
+ def create_consistencygroup(self, context, group):
+ """Creates a consistencygroup."""
+ model_update = {'status': 'available'}
+ return model_update
+
+ def create_consistencygroup_from_src(self, context, group, volumes,
+ cgsnapshot=None, snapshots=None,
+ source_cg=None, source_vols=None):
+ """Creates a consistency group from a source"""
+ LOG.error(_LE("Creating a consistency group from a source is not "
+ "currently supported."))
+ raise NotImplementedError()
+
+ def delete_consistencygroup(self, context, group):
+ """Deletes a consistency group."""
+ # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
+ # Will fix in M release
+ volumes = self.db.volume_get_all_by_group(context, group.id)
+ for volume in volumes:
+ self.delete_volume(volume)
+ volume.status = 'deleted'
+
+ model_update = {'status': group.status}
+
+ return model_update, volumes
+
+ def update_consistencygroup(self, context, group,
+ add_volumes=None, remove_volumes=None):
+ """Updates a consistency group.
+
+ Because the backend has no concept of volume grouping, cinder will
+ maintain all volume/consistency group relationships. Because of this
+ functionality, there is no need to make any client calls; instead
+ simply returning out of this function allows cinder to properly
+ add/remove volumes from the consistency group.
+ """
+ return None, None, None
+
+ def create_cgsnapshot(self, context, cgsnapshot):
+ """Creates a consistency group snapshot."""
+ client = self._login()
+ try:
+ # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
+ # Will fix in M release
+ snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
+
+ snap_set = []
+ snapshot_base_name = "snapshot-" + cgsnapshot['id']
+ for i, snapshot in enumerate(snapshots):
+ volume = snapshot.volume
+ volume_name = volume['name']
+ try:
+ volume_info = client.getVolumeByName(volume_name)
+ except Exception as ex:
+ error = six.text_type(ex)
+ LOG.error(_LE("Could not find volume with name %(name)s. "
+ "Error: %(error)s"),
+ {'name': volume_name,
+ 'error': error})
+ raise exception.VolumeBackendAPIException(data=error)
+
+ volume_id = volume_info['id']
+ snapshot_name = snapshot_base_name + "-" + six.text_type(i)
+ snap_set_member = {'volumeName': volume_name,
+ 'volumeId': volume_id,
+ 'snapshotName': snapshot_name}
+ snap_set.append(snap_set_member)
+ snapshot.status = 'available'
+
+ source_volume_id = snap_set[0]['volumeId']
+ optional = {'inheritAccess': True}
+ description = cgsnapshot.get('description', None)
+ if description:
+ optional['description'] = description
+
+ try:
+ client.createSnapshotSet(source_volume_id, snap_set, optional)
+ except Exception as ex:
+ error = six.text_type(ex)
+ LOG.error(_LE("Could not create snapshot set. Error: '%s'"),
+ error)
+ raise exception.VolumeBackendAPIException(
+ data=error)
+
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(data=six.text_type(ex))
+ finally:
+ self._logout(client)
+
+ model_update = {'status': 'available'}
+
+ return model_update, snapshots
+
+ def delete_cgsnapshot(self, context, cgsnapshot):
+ """Deletes a consistency group snapshot."""
+
+ client = self._login()
+ try:
+ snap_name_base = "snapshot-" + cgsnapshot['id']
+
+ # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
+ # Will fix in M release
+ snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
+
+ for i, snapshot in enumerate(snapshots):
+ try:
+ snap_name = snap_name_base + "-" + six.text_type(i)
+ snap_info = client.getSnapshotByName(snap_name)
+ client.deleteSnapshot(snap_info['id'])
+ except hpexceptions.HTTPNotFound:
+ LOG.error(_LE("Snapshot did not exist. It will not be "
+ "deleted."))
+ except hpexceptions.HTTPServerError as ex:
+ in_use_msg = ('cannot be deleted because it is a clone '
+ 'point')
+ if in_use_msg in ex.get_description():
+ raise exception.SnapshotIsBusy(snapshot_name=snap_name)
+
+ raise exception.VolumeBackendAPIException(
+ data=six.text_type(ex))
+
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(
+ data=six.text_type(ex))
+ finally:
+ self._logout(client)
+
+ model_update = {'status': cgsnapshot['status']}
+
+ return model_update, snapshots
+
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
client = self._login()
data['total_volumes'] = total_volumes
data['filter_function'] = self.get_filter_function()
data['goodness_function'] = self.get_goodness_function()
+ if hplefthandclient.version >= MIN_CG_CLIENT_VERSION:
+ data['consistencygroup_support'] = True
self.device_stats = data