]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
LeftHand: Adding Consistency Group Support
authorAlex O'Rourke <alex.orourke@hp.com>
Thu, 6 Aug 2015 16:21:58 +0000 (09:21 -0700)
committerAlex O'Rourke <alex.orourke@hp.com>
Thu, 27 Aug 2015 20:47:13 +0000 (13:47 -0700)
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
cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py
cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py

index fbf1264f99376a839585fa92059a1367944d5640..affc53ed6e67d0d048455966998a7a4727633ea8 100644 (file)
@@ -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'])
index 0a065207c15d1c2c5d3ff91e056821996f0d8dd3..22a64d86a607c2e3248c5d86f274b1943ec6c37d 100644 (file)
@@ -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)
index ff32fbe0dde01d83bfe01f3e8bc0aed303e38022..cf20373678fb69c394fae7f2315a5bdd2b4956b0 100644 (file)
@@ -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