From 3a62a3ebbfed8128f8b4fcefbf4c9f670d2c1c8e Mon Sep 17 00:00:00 2001 From: Alex O'Rourke Date: Thu, 6 Aug 2015 09:21:58 -0700 Subject: [PATCH] LeftHand: Adding Consistency Group Support Adds support for Consistency Groups to the HP LeftHand driver create_consistencygroup_from_src will not be implemented until the newest version of the LeftHand API is released Implements: blueprint hp-lefthand-add-consistency-groups Change-Id: Id86e86f9bee4e5ce80d739a586037c989e73e379 --- cinder/tests/unit/test_hplefthand.py | 236 +++++++++++++++++- .../drivers/san/hp/hp_lefthand_iscsi.py | 36 ++- .../drivers/san/hp/hp_lefthand_rest_proxy.py | 141 ++++++++++- 3 files changed, 409 insertions(+), 4 deletions(-) diff --git a/cinder/tests/unit/test_hplefthand.py b/cinder/tests/unit/test_hplefthand.py index fbf1264f9..affc53ed6 100644 --- a/cinder/tests/unit/test_hplefthand.py +++ b/cinder/tests/unit/test_hplefthand.py @@ -662,6 +662,14 @@ class TestHPLeftHandCLIQISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 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'), @@ -695,8 +703,9 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): _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 @@ -2072,3 +2081,228 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 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']) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py index 0a065207c..22a64d86a 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py @@ -51,7 +51,8 @@ class HPLeftHandISCSIDriver(driver.TransferVD, driver.CloneableVD, driver.SnapshotVD, driver.MigrateVD, - driver.BaseVD): + driver.BaseVD, + driver.ConsistencyGroupVD): """Executes commands relating to HP/LeftHand SAN ISCSI volumes. Version history: @@ -63,9 +64,10 @@ class HPLeftHandISCSIDriver(driver.TransferVD, 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) @@ -115,6 +117,36 @@ class HPLeftHandISCSIDriver(driver.TransferVD, """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) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py index ff32fbe0d..cf2037367 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py @@ -24,6 +24,7 @@ from oslo_utils import units 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 @@ -69,6 +70,7 @@ CONF = cfg.CONF 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 = { @@ -106,9 +108,10 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): 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 = {} @@ -121,6 +124,7 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): # 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) @@ -248,6 +252,139 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): 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() @@ -339,6 +476,8 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): 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 -- 2.45.2