]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp: Add Consistency Group support for E-Series
authorMichael Price <michael.price@netapp.com>
Fri, 8 Jan 2016 17:04:35 +0000 (11:04 -0600)
committerMichael Price <michael.price@netapp.com>
Fri, 26 Feb 2016 16:27:48 +0000 (16:27 +0000)
Add Consistency Group support to the E-Series driver. This
implementation utilizes the native Consistency Group feature
available on the E-Series backend to support Cinder
Consistency Groups.

CGs and standalone snapshots both utilize snapshot groups.
There is a limit of 3 snapshot groups per volume, so the number
of standalone snapshots will be limited by the number of
consistency groups that are created, and likewise the reverse.
Each CG/Snapshot Group will support up to 32 snapshots, so each
CG that a volume is a part of will reduce the number of available
standalone snapshots that can be created by 32 (from a maximum
of 96).

Implements: blueprint netapp-eseries-consistency-groups
Change-Id: Ib0fc9fa9abc6699f2971948d3d4c5e9902381072

cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py
cinder/volume/drivers/netapp/eseries/client.py
cinder/volume/drivers/netapp/eseries/fc_driver.py
cinder/volume/drivers/netapp/eseries/iscsi_driver.py
cinder/volume/drivers/netapp/eseries/library.py
releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml [new file with mode: 0644]

index a745cd7c7d4d3aba40598685c85033df9d3230f6..7a8ff129d5dc813a9a2df864800452613897109b 100644 (file)
@@ -48,6 +48,15 @@ FAKE_CINDER_SNAPSHOT = {
     'volume': FAKE_CINDER_VOLUME
 }
 
+FAKE_CINDER_CG = {
+    'id': '78f95b9d-3f02-4781-a512-1a1c951d48a2',
+}
+
+FAKE_CINDER_CG_SNAPSHOT = {
+    'id': '78f95b9d-4d13-4781-a512-1a1c951d6a6',
+    'consistencygroup_id': FAKE_CINDER_CG['id'],
+}
+
 MULTIATTACH_HOST_GROUP = {
     'clusterRef': '8500000060080E500023C7340036035F515B78FC',
     'label': utils.MULTI_ATTACH_HOST_GROUP_NAME,
@@ -693,7 +702,8 @@ SNAPSHOT_IMAGE = {
     'activeCOW': True,
     'isRollbackSource': False,
     'pitRef': '3400000060080E500023BB3400631F335294A5A8',
-    'pitSequenceNumber': '19'
+    'pitSequenceNumber': '19',
+    'consistencyGroupId': '0000000000000000000000000000000000000000',
 }
 
 SNAPSHOT_VOLUME = {
@@ -1011,6 +1021,46 @@ FAKE_CLIENT_PARAMS = {
     'password': 'rw',
 }
 
+FAKE_CONSISTENCY_GROUP = {
+    'cgRef': '2A000000600A0980006077F8008702F45480F41A',
+    'label': '5BO5GPO4PFGRPMQWEXGTILSAUI',
+    'repFullPolicy': 'failbasewrites',
+    'fullWarnThreshold': 75,
+    'autoDeleteLimit': 0,
+    'rollbackPriority': 'medium',
+    'uniqueSequenceNumber': [8940, 8941, 8942],
+    'creationPendingStatus': 'none',
+    'name': '5BO5GPO4PFGRPMQWEXGTILSAUI',
+    'id': '2A000000600A0980006077F8008702F45480F41A'
+}
+
+FAKE_CONSISTENCY_GROUP_MEMBER = {
+    'consistencyGroupId': '2A000000600A0980006077F8008702F45480F41A',
+    'volumeId': '02000000600A0980006077F8000002F55480F421',
+    'volumeWwn': '600A0980006077F8000002F55480F421',
+    'baseVolumeName': 'I5BHHNILUJGZHEUD4S36GCOQYA',
+    'clusterSize': 65536,
+    'totalRepositoryVolumes': 1,
+    'totalRepositoryCapacity': '4294967296',
+    'usedRepositoryCapacity': '5636096',
+    'fullWarnThreshold': 75,
+    'totalSnapshotImages': 3,
+    'totalSnapshotVolumes': 2,
+    'autoDeleteSnapshots': False,
+    'autoDeleteLimit': 0,
+    'pitGroupId': '33000000600A0980006077F8000002F85480F435',
+    'repositoryVolume': '36000000600A0980006077F8000002F75480F435'
+}
+FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME = {
+    'id': '2C00000060080E500034194F002C96A256BD50F9',
+    'name': '6TRZHKDG75DVLBC2JU5J647RME',
+    'cgViewRef': '2C00000060080E500034194F002C96A256BD50F9',
+    'groupRef': '2A00000060080E500034194F0087969856BD2D67',
+    'label': '6TRZHKDG75DVLBC2JU5J647RME',
+    'viewTime': '1455221060',
+    'viewSequenceNumber': '10',
+}
+
 
 def list_snapshot_groups(numGroups):
     snapshots = []
@@ -1179,9 +1229,12 @@ class FakeEseriesClient(object):
     def list_snapshot_images(self):
         return [SNAPSHOT_IMAGE]
 
-    def list_snapshot_image(self):
+    def list_snapshot_image(self, *args, **kwargs):
         return SNAPSHOT_IMAGE
 
+    def create_cg_snapshot_view(self, *args, **kwargs):
+        return SNAPSHOT_VOLUME
+
     def list_host_types(self):
         return [
             {
@@ -1277,8 +1330,32 @@ class FakeEseriesClient(object):
     def restart_snapshot_volume(self, *args, **kwargs):
         pass
 
+    def create_consistency_group(self, *args, **kwargs):
+        return FAKE_CONSISTENCY_GROUP
+
+    def delete_consistency_group(self, *args, **kwargs):
+        pass
+
+    def list_consistency_groups(self, *args, **kwargs):
+        return [FAKE_CONSISTENCY_GROUP]
+
+    def remove_consistency_group_member(self, *args, **kwargs):
+        pass
+
+    def add_consistency_group_member(self, *args, **kwargs):
+        pass
+
     def list_backend_store(self, key):
         return {}
 
     def save_backend_store(self, key, val):
         pass
+
+    def create_consistency_group_snapshot(self, *args, **kwargs):
+        return [SNAPSHOT_IMAGE]
+
+    def get_consistency_group_snapshots(self, *args, **kwargs):
+        return [SNAPSHOT_IMAGE]
+
+    def delete_consistency_group_snapshot(self, *args, **kwargs):
+        pass
index a58ef5a11bc74e6ec7927a5de8c577375129eb04..5ebe3fc818e0934fafd4646f7dab629bae3dd7a8 100644 (file)
@@ -906,6 +906,162 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
             'DELETE', self.my_client.RESOURCE_PATHS['snapshot_image'],
             **{'object-id': fake_ref})
 
+    def test_create_consistency_group(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        name = 'fake'
+
+        self.my_client.create_consistency_group(name)
+
+        invoke.assert_called_once_with(
+            'POST', self.my_client.RESOURCE_PATHS['cgroups'], mock.ANY)
+
+    def test_list_consistency_group(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        ref = 'fake'
+
+        self.my_client.get_consistency_group(ref)
+
+        invoke.assert_called_once_with(
+            'GET', self.my_client.RESOURCE_PATHS['cgroup'],
+            **{'object-id': ref})
+
+    def test_list_consistency_groups(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+
+        self.my_client.list_consistency_groups()
+
+        invoke.assert_called_once_with(
+            'GET', self.my_client.RESOURCE_PATHS['cgroups'])
+
+    def test_delete_consistency_group(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        ref = 'fake'
+
+        self.my_client.delete_consistency_group(ref)
+
+        invoke.assert_called_once_with(
+            'DELETE', self.my_client.RESOURCE_PATHS['cgroup'],
+            **{'object-id': ref})
+
+    def test_add_consistency_group_member(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        vol_id = eseries_fake.VOLUME['id']
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.my_client.add_consistency_group_member(vol_id, cg_id)
+
+        invoke.assert_called_once_with(
+            'POST', self.my_client.RESOURCE_PATHS['cgroup_members'],
+            mock.ANY, **{'object-id': cg_id})
+
+    def test_remove_consistency_group_member(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        vol_id = eseries_fake.VOLUME['id']
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.my_client.remove_consistency_group_member(vol_id, cg_id)
+
+        invoke.assert_called_once_with(
+            'DELETE', self.my_client.RESOURCE_PATHS['cgroup_member'],
+            **{'object-id': cg_id, 'vol-id': vol_id})
+
+    def test_create_consistency_group_snapshot(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshots')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.my_client.create_consistency_group_snapshot(cg_id)
+
+        invoke.assert_called_once_with('POST', path, **{'object-id': cg_id})
+
+    @ddt.data(0, 32)
+    def test_delete_consistency_group_snapshot(self, seq_num):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshot')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.my_client.delete_consistency_group_snapshot(cg_id, seq_num)
+
+        invoke.assert_called_once_with(
+            'DELETE', path, **{'object-id': cg_id, 'seq-num': seq_num})
+
+    def test_get_consistency_group_snapshots(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshots')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.my_client.get_consistency_group_snapshots(cg_id)
+
+        invoke.assert_called_once_with(
+            'GET', path, **{'object-id': cg_id})
+
+    def test_create_cg_snapshot_view(self):
+        cg_snap_view = copy.deepcopy(
+            eseries_fake.FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME)
+        view = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
+        invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
+            return_value=cg_snap_view))
+        list_views = self.mock_object(
+            self.my_client, 'list_cg_snapshot_views',
+            mock.Mock(return_value=[view]))
+        name = view['name']
+        snap_id = view['basePIT']
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_cgsnap_views')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.my_client.create_cg_snapshot_view(cg_id, name, snap_id)
+
+        invoke.assert_called_once_with(
+            'POST', path, mock.ANY, **{'object-id': cg_id})
+        list_views.assert_called_once_with(cg_id, cg_snap_view['cgViewRef'])
+
+    def test_create_cg_snapshot_view_not_found(self):
+        cg_snap_view = copy.deepcopy(
+            eseries_fake.FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME)
+        view = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
+        invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
+            return_value=cg_snap_view))
+        list_views = self.mock_object(
+            self.my_client, 'list_cg_snapshot_views',
+            mock.Mock(return_value=[view]))
+        del_view = self.mock_object(self.my_client, 'delete_cg_snapshot_view')
+        name = view['name']
+        # Ensure we don't get a match on the retrieved views
+        snap_id = None
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_cgsnap_views')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+
+        self.assertRaises(
+            exception.NetAppDriverException,
+            self.my_client.create_cg_snapshot_view, cg_id, name, snap_id)
+
+        invoke.assert_called_once_with(
+            'POST', path, mock.ANY, **{'object-id': cg_id})
+        list_views.assert_called_once_with(cg_id, cg_snap_view['cgViewRef'])
+        del_view.assert_called_once_with(cg_id, cg_snap_view['id'])
+
+    def test_list_cg_snapshot_views(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshot_views')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+        view_id = 'id'
+
+        self.my_client.list_cg_snapshot_views(cg_id, view_id)
+
+        invoke.assert_called_once_with(
+            'GET', path, **{'object-id': cg_id, 'view-id': view_id})
+
+    def test_delete_cg_snapshot_view(self):
+        invoke = self.mock_object(self.my_client, '_invoke')
+        path = self.my_client.RESOURCE_PATHS.get('cgroup_snap_view')
+        cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id']
+        view_id = 'id'
+
+        self.my_client.delete_cg_snapshot_view(cg_id, view_id)
+
+        invoke.assert_called_once_with(
+            'DELETE', path, **{'object-id': cg_id, 'view-id': view_id})
+
     @ddt.data('00.00.00.00', '01.52.9000.2', '01.52.9001.2', '01.51.9000.3',
               '01.51.9001.3', '01.51.9010.5', '0.53.9000.3', '0.53.9001.4')
     def test_api_version_not_support_asup(self, api_version):
index 5ad1c499bdf9a22fd2bdf06542d87b68b258b48d..b4910152668fb067b478f1dbf53bfe7f9f24ee80 100644 (file)
@@ -488,3 +488,70 @@ class NetAppESeriesDriverTestCase(object):
         self.driver.extend_volume(self.fake_ret_vol, capacity)
         self.library.extend_volume.assert_called_with(self.fake_ret_vol,
                                                       capacity)
+
+    @mock.patch.object(library.NetAppESeriesLibrary,
+                       'create_cgsnapshot', mock.Mock())
+    def test_create_cgsnapshot(self):
+        cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT)
+        snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE])
+
+        self.driver.create_cgsnapshot('ctx', cgsnapshot, snapshots)
+
+        self.library.create_cgsnapshot.assert_called_with(cgsnapshot,
+                                                          snapshots)
+
+    @mock.patch.object(library.NetAppESeriesLibrary,
+                       'delete_cgsnapshot', mock.Mock())
+    def test_delete_cgsnapshot(self):
+        cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT)
+        snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE])
+
+        self.driver.delete_cgsnapshot('ctx', cgsnapshot, snapshots)
+
+        self.library.delete_cgsnapshot.assert_called_with(cgsnapshot,
+                                                          snapshots)
+
+    @mock.patch.object(library.NetAppESeriesLibrary,
+                       'create_consistencygroup', mock.Mock())
+    def test_create_consistencygroup(self):
+        cg = copy.deepcopy(fakes.FAKE_CINDER_CG)
+
+        self.driver.create_consistencygroup('ctx', cg)
+
+        self.library.create_consistencygroup.assert_called_with(cg)
+
+    @mock.patch.object(library.NetAppESeriesLibrary,
+                       'delete_consistencygroup', mock.Mock())
+    def test_delete_consistencygroup(self):
+        cg = copy.deepcopy(fakes.FAKE_CINDER_CG)
+        volumes = copy.deepcopy([fakes.VOLUME])
+
+        self.driver.delete_consistencygroup('ctx', cg, volumes)
+
+        self.library.delete_consistencygroup.assert_called_with(cg, volumes)
+
+    @mock.patch.object(library.NetAppESeriesLibrary,
+                       'update_consistencygroup', mock.Mock())
+    def test_update_consistencygroup(self):
+        group = copy.deepcopy(fakes.FAKE_CINDER_CG)
+
+        self.driver.update_consistencygroup('ctx', group, {}, {})
+
+        self.library.update_consistencygroup.assert_called_with(group, {}, {})
+
+    @mock.patch.object(library.NetAppESeriesLibrary,
+                       'create_consistencygroup_from_src', mock.Mock())
+    def test_create_consistencygroup_from_src(self):
+        cg = copy.deepcopy(fakes.FAKE_CINDER_CG)
+        volumes = copy.deepcopy([fakes.VOLUME])
+        source_vols = copy.deepcopy([fakes.VOLUME])
+        cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT)
+        source_cg = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT)
+        snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE])
+
+        self.driver.create_consistencygroup_from_src(
+            'ctx', cg, volumes, cgsnapshot, snapshots, source_cg,
+            source_vols)
+
+        self.library.create_consistencygroup_from_src.assert_called_with(
+            cg, volumes, cgsnapshot, snapshots, source_cg, source_vols)
index 8d7dd051e86e6888ae163dff9e6910300ec21084..b8ff667533ba882e32d6a422de60195c9339aee6 100644 (file)
@@ -46,7 +46,7 @@ from cinder.zonemanager import utils as fczm_utils
 
 
 def get_fake_volume():
-    """Return a fake Cinder Volume that can be used a parameter"""
+    """Return a fake Cinder Volume that can be used as a parameter"""
     return {
         'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1,
         'volume_name': 'lun1', 'host': 'hostname@backend#DDP',
@@ -326,6 +326,7 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
             thin_provisioned = pool['thinProvisioningCapable']
 
             expected = {
+                'consistencygroup_support': True,
                 'netapp_disk_encryption':
                     six.text_type(pool['encrypted']).lower(),
                 'netapp_eseries_flash_read_cache':
@@ -1063,6 +1064,219 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
             fake_volume["id"])
         self.assertEqual(2, library.LOG.error.call_count)
 
+    def test_create_consistencygroup(self):
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        expected = {'status': 'available'}
+        create_cg = self.mock_object(self.library,
+                                     '_create_consistency_group',
+                                     mock.Mock(return_value=expected))
+
+        actual = self.library.create_consistencygroup(fake_cg)
+
+        create_cg.assert_called_once_with(fake_cg)
+        self.assertEqual(expected, actual)
+
+    def test_create_consistency_group(self):
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        expected = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        create_cg = self.mock_object(self.library._client,
+                                     'create_consistency_group',
+                                     mock.Mock(return_value=expected))
+
+        result = self.library._create_consistency_group(fake_cg)
+
+        name = utils.convert_uuid_to_es_fmt(fake_cg['id'])
+        create_cg.assert_called_once_with(name)
+        self.assertEqual(expected, result)
+
+    def test_delete_consistencygroup(self):
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        volumes = [get_fake_volume()] * 3
+        model_update = {'status': 'deleted'}
+        volume_update = [{'status': 'deleted', 'id': vol['id']} for vol in
+                         volumes]
+        delete_cg = self.mock_object(self.library._client,
+                                     'delete_consistency_group')
+        updt_index = self.mock_object(
+            self.library, '_merge_soft_delete_changes')
+        delete_vol = self.mock_object(self.library, 'delete_volume')
+        self.mock_object(self.library, '_get_consistencygroup',
+                         mock.Mock(return_value=cg))
+
+        result = self.library.delete_consistencygroup(fake_cg, volumes)
+
+        self.assertEqual(len(volumes), delete_vol.call_count)
+        delete_cg.assert_called_once_with(cg['id'])
+        self.assertEqual((model_update, volume_update), result)
+        updt_index.assert_called_once_with(None, [cg['id']])
+
+    def test_delete_consistencygroup_index_update_failure(self):
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        volumes = [get_fake_volume()] * 3
+        model_update = {'status': 'deleted'}
+        volume_update = [{'status': 'deleted', 'id': vol['id']} for vol in
+                         volumes]
+        delete_cg = self.mock_object(self.library._client,
+                                     'delete_consistency_group')
+        delete_vol = self.mock_object(self.library, 'delete_volume')
+        self.mock_object(self.library, '_get_consistencygroup',
+                         mock.Mock(return_value=cg))
+
+        result = self.library.delete_consistencygroup(fake_cg, volumes)
+
+        self.assertEqual(len(volumes), delete_vol.call_count)
+        delete_cg.assert_called_once_with(cg['id'])
+        self.assertEqual((model_update, volume_update), result)
+
+    def test_delete_consistencygroup_not_found(self):
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        delete_cg = self.mock_object(self.library._client,
+                                     'delete_consistency_group')
+        updt_index = self.mock_object(
+            self.library, '_merge_soft_delete_changes')
+        delete_vol = self.mock_object(self.library, 'delete_volume')
+        exc = exception.ConsistencyGroupNotFound(consistencygroup_id='')
+        self.mock_object(self.library, '_get_consistencygroup',
+                         mock.Mock(side_effect=exc))
+
+        self.library.delete_consistencygroup(fake_cg, [])
+
+        delete_cg.assert_not_called()
+        delete_vol.assert_not_called()
+        updt_index.assert_not_called()
+
+    def test_get_consistencygroup(self):
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        name = utils.convert_uuid_to_es_fmt(fake_cg['id'])
+        cg['name'] = name
+        list_cgs = self.mock_object(self.library._client,
+                                    'list_consistency_groups',
+                                    mock.Mock(return_value=[cg]))
+
+        result = self.library._get_consistencygroup(fake_cg)
+
+        self.assertEqual(cg, result)
+        list_cgs.assert_called_once_with()
+
+    def test_get_consistencygroup_not_found(self):
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        list_cgs = self.mock_object(self.library._client,
+                                    'list_consistency_groups',
+                                    mock.Mock(return_value=[cg]))
+
+        self.assertRaises(exception.ConsistencyGroupNotFound,
+                          self.library._get_consistencygroup,
+                          copy.deepcopy(eseries_fake.FAKE_CINDER_CG))
+
+        list_cgs.assert_called_once_with()
+
+    def test_update_consistencygroup(self):
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        vol = copy.deepcopy(eseries_fake.VOLUME)
+        volumes = [get_fake_volume()] * 3
+        self.mock_object(
+            self.library, '_get_volume', mock.Mock(return_value=vol))
+        self.mock_object(self.library, '_get_consistencygroup',
+                         mock.Mock(return_value=cg))
+
+        self.library.update_consistencygroup(fake_cg, volumes, volumes)
+
+    def test_create_consistencygroup_from_src(self):
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        volumes = [cinder_utils.create_volume(self.ctxt) for i in range(3)]
+        src_volumes = [cinder_utils.create_volume(self.ctxt) for v in volumes]
+        update_cg = self.mock_object(
+            self.library, '_update_consistency_group_members')
+        create_cg = self.mock_object(
+            self.library, '_create_consistency_group',
+            mock.Mock(return_value=cg))
+        self.mock_object(
+            self.library, '_create_volume_from_snapshot')
+
+        self.mock_object(
+            self.library, '_get_snapshot', mock.Mock(return_value=snap))
+
+        self.library.create_consistencygroup_from_src(
+            fake_cg, volumes, None, None, None, src_volumes)
+
+        create_cg.assert_called_once_with(fake_cg)
+        update_cg.assert_called_once_with(cg, volumes, [])
+
+    def test_create_consistencygroup_from_src_cgsnapshot(self):
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG)
+        fake_vol = cinder_utils.create_volume(self.ctxt)
+        cgsnap = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT)
+        volumes = [fake_vol]
+        snapshots = [cinder_utils.create_snapshot(self.ctxt, v['id']) for v
+                     in volumes]
+        update_cg = self.mock_object(
+            self.library, '_update_consistency_group_members')
+        create_cg = self.mock_object(
+            self.library, '_create_consistency_group',
+            mock.Mock(return_value=cg))
+        clone_vol = self.mock_object(
+            self.library, '_create_volume_from_snapshot')
+
+        self.library.create_consistencygroup_from_src(
+            fake_cg, volumes, cgsnap, snapshots, None, None)
+
+        create_cg.assert_called_once_with(fake_cg)
+        update_cg.assert_called_once_with(cg, volumes, [])
+        self.assertEqual(clone_vol.call_count, len(volumes))
+
+    @ddt.data({'consistencyGroupId': utils.NULL_REF},
+              {'consistencyGroupId': None}, {'consistencyGroupId': '1'}, {})
+    def test_is_cgsnapshot(self, snapshot_image):
+        if snapshot_image.get('consistencyGroupId'):
+            result = not (utils.NULL_REF == snapshot_image[
+                'consistencyGroupId'])
+        else:
+            result = False
+
+        actual = self.library._is_cgsnapshot(snapshot_image)
+
+        self.assertEqual(result, actual)
+
+    @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new=
+                cinder_utils.ZeroIntervalLoopingCall)
+    def test_copy_volume_high_priority_readonly(self):
+        src_vol = copy.deepcopy(eseries_fake.VOLUME)
+        dst_vol = copy.deepcopy(eseries_fake.VOLUME)
+        vc = copy.deepcopy(eseries_fake.VOLUME_COPY_JOB)
+        self.mock_object(self.library._client, 'create_volume_copy_job',
+                         mock.Mock(return_value=vc))
+        self.mock_object(self.library._client, 'list_vol_copy_job',
+                         mock.Mock(return_value=vc))
+        delete_copy = self.mock_object(self.library._client,
+                                       'delete_vol_copy_job')
+
+        result = self.library._copy_volume_high_priority_readonly(
+            src_vol, dst_vol)
+
+        self.assertIsNone(result)
+        delete_copy.assert_called_once_with(vc['volcopyRef'])
+
+    @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new=
+                cinder_utils.ZeroIntervalLoopingCall)
+    def test_copy_volume_high_priority_readonly_job_create_failure(self):
+        src_vol = copy.deepcopy(eseries_fake.VOLUME)
+        dst_vol = copy.deepcopy(eseries_fake.VOLUME)
+        self.mock_object(
+            self.library._client, 'create_volume_copy_job', mock.Mock(
+                side_effect=exception.NetAppDriverException))
+
+        self.assertRaises(
+            exception.NetAppDriverException,
+            self.library._copy_volume_high_priority_readonly, src_vol,
+            dst_vol)
+
 
 @ddt.ddt
 class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
@@ -1231,9 +1445,12 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
             return_value = fake_created_volume)
         fake_cinder_volume = copy.deepcopy(eseries_fake.FAKE_CINDER_VOLUME)
         extend_vol = {'id': uuid.uuid4(), 'size': 10}
+        self.mock_object(self.library, '_create_volume_from_snapshot')
 
         self.library.create_cloned_volume(extend_vol, fake_cinder_volume)
 
+    @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
+                new = cinder_utils.ZeroIntervalLoopingCall)
     def test_create_volume_from_snapshot(self):
         fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
@@ -1273,6 +1490,8 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         self.library._client.delete_volume.assert_called_once_with(
             fake_dest_eseries_volume['volumeRef'])
 
+    @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
+                new = cinder_utils.ZeroIntervalLoopingCall)
     def test_create_volume_from_snapshot_copy_job_fails(self):
         fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         self.mock_object(self.library, "_schedule_and_create_volume",
@@ -1305,6 +1524,8 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         self.library._client.delete_volume.assert_called_once_with(
             fake_dest_eseries_volume['volumeRef'])
 
+    @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
+                new = cinder_utils.ZeroIntervalLoopingCall)
     def test_create_volume_from_snapshot_fail_to_delete_snapshot_volume(self):
         fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
         fake_dest_eseries_volume['volumeRef'] = 'fake_volume_ref'
@@ -1334,6 +1555,42 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         # Ensure the volume we created is not cleaned up
         self.assertEqual(0, self.library._client.delete_volume.call_count)
 
+    def test_create_snapshot_volume_cgsnap(self):
+        image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
+        self.mock_object(self.library, '_get_snapshot_group', mock.Mock(
+            return_value=grp))
+        expected = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
+        self.mock_object(self.library, '_is_cgsnapshot', mock.Mock(
+            return_value=True))
+        create_view = self.mock_object(
+            self.library._client, 'create_cg_snapshot_view',
+            mock.Mock(return_value=expected))
+
+        result = self.library._create_snapshot_volume(image)
+
+        self.assertEqual(expected, result)
+        create_view.assert_called_once_with(image['consistencyGroupId'],
+                                            mock.ANY, image['id'])
+
+    def test_create_snapshot_volume(self):
+        image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
+        self.mock_object(self.library, '_get_snapshot_group', mock.Mock(
+            return_value=grp))
+        expected = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
+        self.mock_object(self.library, '_is_cgsnapshot', mock.Mock(
+            return_value=False))
+        create_view = self.mock_object(
+            self.library._client, 'create_snapshot_volume',
+            mock.Mock(return_value=expected))
+
+        result = self.library._create_snapshot_volume(image)
+
+        self.assertEqual(expected, result)
+        create_view.assert_called_once_with(
+            image['pitRef'], mock.ANY, image['baseVol'])
+
     def test_create_snapshot_group(self):
         label = 'label'
 
@@ -1483,7 +1740,10 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         full_group = copy.deepcopy(snapshot_group)
         full_group['snapshotCount'] = self.library.MAX_SNAPSHOT_COUNT
 
-        snapshot_groups = [snapshot_group, reserved_group, full_group]
+        cgroup = copy.deepcopy(snapshot_group)
+        cgroup['consistencyGroup'] = True
+
+        snapshot_groups = [snapshot_group, reserved_group, full_group, cgroup]
         get_call = self.mock_object(
             self.library, '_get_snapshot_groups_for_volume', mock.Mock(
                 return_value=snapshot_groups))
@@ -1817,6 +2077,194 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
             get_store.assert_not_called()
             save_store.assert_not_called()
 
+    def test_create_cgsnapshot(self):
+        fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT)
+        fake_vol = cinder_utils.create_volume(self.ctxt)
+        fake_snapshots = [cinder_utils.create_snapshot(self.ctxt,
+                                                       fake_vol['id'])]
+        vol = copy.deepcopy(eseries_fake.VOLUME)
+        image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        image['baseVol'] = vol['id']
+        cg_snaps = [image]
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+
+        for snap in cg_snaps:
+            snap['baseVol'] = vol['id']
+        get_cg = self.mock_object(
+            self.library, '_get_consistencygroup_by_name',
+            mock.Mock(return_value=cg))
+        get_vol = self.mock_object(
+            self.library, '_get_volume',
+            mock.Mock(return_value=vol))
+        mk_snap = self.mock_object(
+            self.library._client, 'create_consistency_group_snapshot',
+            mock.Mock(return_value=cg_snaps))
+
+        model_update, snap_updt = self.library.create_cgsnapshot(
+            fake_cgsnapshot, fake_snapshots)
+
+        self.assertIsNone(model_update)
+        for snap in cg_snaps:
+            self.assertIn({'id': fake_snapshots[0]['id'],
+                           'provider_id': snap['id'],
+                           'status': 'available'}, snap_updt)
+        self.assertEqual(len(cg_snaps), len(snap_updt))
+
+        get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt(
+            fake_cgsnapshot['consistencygroup_id']))
+        self.assertEqual(get_vol.call_count, len(fake_snapshots))
+        mk_snap.assert_called_once_with(cg['id'])
+
+    def test_create_cgsnapshot_cg_fail(self):
+        fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT)
+        fake_snapshots = [copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)]
+        self.mock_object(
+            self.library, '_get_consistencygroup_by_name',
+            mock.Mock(side_effect=exception.NetAppDriverException))
+
+        self.assertRaises(
+            exception.NetAppDriverException,
+            self.library.create_cgsnapshot, fake_cgsnapshot, fake_snapshots)
+
+    def test_delete_cgsnapshot(self):
+        """Test the deletion of a cgsnapshot when a soft delete is required"""
+        fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT)
+        fake_vol = cinder_utils.create_volume(self.ctxt)
+        fake_snapshots = [cinder_utils.create_snapshot(
+            self.ctxt, fake_vol['id'])]
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        # Ensure that the snapshot to be deleted is not the oldest
+        cg_snap['pitSequenceNumber'] = str(max(cg['uniqueSequenceNumber']))
+        cg_snaps = [cg_snap]
+        for snap in fake_snapshots:
+            snap['provider_id'] = cg_snap['id']
+        vol = copy.deepcopy(eseries_fake.VOLUME)
+        for snap in cg_snaps:
+            snap['baseVol'] = vol['id']
+        get_cg = self.mock_object(
+            self.library, '_get_consistencygroup_by_name',
+            mock.Mock(return_value=cg))
+        self.mock_object(
+            self.library._client, 'delete_consistency_group_snapshot')
+        self.mock_object(
+            self.library._client, 'get_consistency_group_snapshots',
+            mock.Mock(return_value=cg_snaps))
+        soft_del = self.mock_object(
+            self.library, '_soft_delete_cgsnapshot',
+            mock.Mock(return_value=(None, None)))
+
+        # Mock the locking mechanism
+        model_update, snap_updt = self.library.delete_cgsnapshot(
+            fake_cgsnapshot, fake_snapshots)
+
+        self.assertIsNone(model_update)
+        self.assertIsNone(snap_updt)
+        get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt(
+            fake_cgsnapshot['consistencygroup_id']))
+        soft_del.assert_called_once_with(
+            cg, cg_snap['pitSequenceNumber'])
+
+    @ddt.data(True, False)
+    def test_soft_delete_cgsnapshot(self, bitset_exists):
+        """Test the soft deletion of a cgsnapshot"""
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        seq_num = 10
+        cg_snap['pitSequenceNumber'] = seq_num
+        cg_snaps = [cg_snap]
+        self.mock_object(
+            self.library._client, 'delete_consistency_group_snapshot')
+        self.mock_object(
+            self.library._client, 'get_consistency_group_snapshots',
+            mock.Mock(return_value=cg_snaps))
+        bitset = na_utils.BitSet(1)
+        index = {cg['id']: repr(bitset)} if bitset_exists else {}
+        bitset >>= len(cg_snaps)
+        updt = {cg['id']: repr(bitset)}
+        self.mock_object(self.library, '_get_soft_delete_map', mock.Mock(
+            return_value=index))
+        save_map = self.mock_object(
+            self.library, '_merge_soft_delete_changes')
+
+        model_update, snap_updt = self.library._soft_delete_cgsnapshot(
+            cg, seq_num)
+
+        self.assertIsNone(model_update)
+        self.assertIsNone(snap_updt)
+        save_map.assert_called_once_with(updt, None)
+
+    def test_delete_cgsnapshot_single(self):
+        """Test the backend deletion of the oldest cgsnapshot"""
+        fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT)
+        fake_vol = cinder_utils.create_volume(self.ctxt)
+        fake_snapshots = [cinder_utils.create_snapshot(self.ctxt,
+                                                       fake_vol['id'])]
+        cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        cg_snaps = [cg_snap]
+        for snap in fake_snapshots:
+            snap['provider_id'] = cg_snap['id']
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        cg['uniqueSequenceNumber'] = [cg_snap['pitSequenceNumber']]
+        vol = copy.deepcopy(eseries_fake.VOLUME)
+        for snap in cg_snaps:
+            snap['baseVol'] = vol['id']
+        get_cg = self.mock_object(
+            self.library, '_get_consistencygroup_by_name',
+            mock.Mock(return_value=cg))
+        del_snap = self.mock_object(
+            self.library._client, 'delete_consistency_group_snapshot',
+            mock.Mock(return_value=cg_snaps))
+
+        model_update, snap_updt = self.library.delete_cgsnapshot(
+            fake_cgsnapshot, fake_snapshots)
+
+        self.assertIsNone(model_update)
+        self.assertIsNone(snap_updt)
+        get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt(
+            fake_cgsnapshot['consistencygroup_id']))
+        del_snap.assert_called_once_with(cg['id'], cg_snap[
+            'pitSequenceNumber'])
+
+    def test_delete_cgsnapshot_snap_not_found(self):
+        fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT)
+        fake_vol = cinder_utils.create_volume(self.ctxt)
+        fake_snapshots = [cinder_utils.create_snapshot(
+            self.ctxt, fake_vol['id'])]
+        cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
+        cg_snaps = [cg_snap]
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        self.mock_object(self.library, '_get_consistencygroup_by_name',
+                         mock.Mock(return_value=cg))
+        self.mock_object(
+            self.library._client, 'delete_consistency_group_snapshot',
+            mock.Mock(return_value=cg_snaps))
+
+        self.assertRaises(
+            exception.CgSnapshotNotFound,
+            self.library.delete_cgsnapshot, fake_cgsnapshot, fake_snapshots)
+
+    @ddt.data(0, 1, 10, 32)
+    def test_cleanup_cg_snapshots(self, count):
+        # Set the soft delete bit for 'count' snapshot images
+        bitset = na_utils.BitSet()
+        for i in range(count):
+            bitset.set(i)
+        cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP)
+        # Define 32 snapshots for the CG
+        cg['uniqueSequenceNumber'] = list(range(32))
+        cg_id = cg['id']
+        del_snap = self.mock_object(
+            self.library._client, 'delete_consistency_group_snapshot')
+        expected_bitset = copy.deepcopy(bitset) >> count
+        expected_updt = {cg_id: repr(expected_bitset)}
+
+        updt = self.library._cleanup_cg_snapshots(
+            cg_id, cg['uniqueSequenceNumber'], bitset)
+
+        self.assertEqual(count, del_snap.call_count)
+        self.assertEqual(expected_updt, updt)
+
     @ddt.data(False, True)
     def test_get_pool_operation_progress(self, expect_complete):
         """Validate the operation progress is interpreted correctly"""
index 49dcc5e66a8036b09120679e305094c853ceb869..0d869f9d5672c2c640e6ffaa6f47215da5068f02 100644 (file)
@@ -126,6 +126,33 @@ class RestClient(WebserviceClient):
         'snapshot_images': '/storage-systems/{system-id}/snapshot-images',
         'snapshot_image':
             '/storage-systems/{system-id}/snapshot-images/{object-id}',
+        'cgroup':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}',
+        'cgroups': '/storage-systems/{system-id}/consistency-groups',
+        'cgroup_members':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/member-volumes',
+        'cgroup_member':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/member-volumes/{vol-id}',
+        'cgroup_snapshots':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/snapshots',
+        'cgroup_snapshot':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/snapshots/{seq-num}',
+        'cgroup_snapshots_by_seq':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/snapshots/{seq-num}',
+        'cgroup_cgsnap_view':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/views/{seq-num}',
+        'cgroup_cgsnap_views':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/views/',
+        'cgroup_snapshot_views':
+            '/storage-systems/{system-id}/consistency-groups/{object-id}'
+            '/views/{view-id}/views',
         'persistent-stores': '/storage-systems/{'
                              'system-id}/persistent-records/',
         'persistent-store': '/storage-systems/{'
@@ -447,6 +474,131 @@ class RestClient(WebserviceClient):
         data = {'name': label}
         return self._invoke('POST', path, data, **{'object-id': object_id})
 
+    def create_consistency_group(self, name, warn_at_percent_full=75,
+                                 rollback_priority='medium',
+                                 full_policy='failbasewrites'):
+        """Define a new consistency group"""
+        path = self.RESOURCE_PATHS.get('cgroups')
+        data = {
+            'name': name,
+            'fullWarnThresholdPercent': warn_at_percent_full,
+            'repositoryFullPolicy': full_policy,
+            # A non-zero threshold enables auto-deletion
+            'autoDeleteThreshold': 0,
+            'rollbackPriority': rollback_priority,
+        }
+
+        return self._invoke('POST', path, data)
+
+    def get_consistency_group(self, object_id):
+        """Retrieve the consistency group identified by object_id"""
+        path = self.RESOURCE_PATHS.get('cgroup')
+
+        return self._invoke('GET', path, **{'object-id': object_id})
+
+    def list_consistency_groups(self):
+        """Retrieve all consistency groups defined on the array"""
+        path = self.RESOURCE_PATHS.get('cgroups')
+
+        return self._invoke('GET', path)
+
+    def delete_consistency_group(self, object_id):
+        path = self.RESOURCE_PATHS.get('cgroup')
+
+        self._invoke('DELETE', path, **{'object-id': object_id})
+
+    def add_consistency_group_member(self, volume_id, cg_id,
+                                     repo_percent=20.0):
+        """Add a volume to a consistency group
+
+        :param volume_id the eseries volume id
+        :param cg_id: the eseries cg id
+        :param repo_percent: percentage capacity of the volume to use for
+        capacity of the copy-on-write repository
+        """
+        path = self.RESOURCE_PATHS.get('cgroup_members')
+        data = {'volumeId': volume_id, 'repositoryPercent': repo_percent}
+
+        return self._invoke('POST', path, data, **{'object-id': cg_id})
+
+    def remove_consistency_group_member(self, volume_id, cg_id):
+        """Remove a volume from a consistency group"""
+        path = self.RESOURCE_PATHS.get('cgroup_member')
+
+        self._invoke('DELETE', path, **{'object-id': cg_id,
+                                        'vol-id': volume_id})
+
+    def create_consistency_group_snapshot(self, cg_id):
+        """Define a consistency group snapshot"""
+        path = self.RESOURCE_PATHS.get('cgroup_snapshots')
+
+        return self._invoke('POST', path, **{'object-id': cg_id})
+
+    def delete_consistency_group_snapshot(self, cg_id, seq_num):
+        """Define a consistency group snapshot"""
+        path = self.RESOURCE_PATHS.get('cgroup_snapshot')
+
+        return self._invoke('DELETE', path, **{'object-id': cg_id,
+                                               'seq-num': seq_num})
+
+    def get_consistency_group_snapshots(self, cg_id):
+        """Retrieve all snapshots defined for a consistency group"""
+        path = self.RESOURCE_PATHS.get('cgroup_snapshots')
+
+        return self._invoke('GET', path, **{'object-id': cg_id})
+
+    def create_cg_snapshot_view(self, cg_id, name, snap_id):
+        """Define a snapshot view for the cgsnapshot
+
+        In order to define a snapshot view for a snapshot defined under a
+        consistency group, the view must be defined at the cgsnapshot
+        level.
+
+        :param cg_id: E-Series cg identifier
+        :param name: the label for the view
+        :param snap_id: E-Series snapshot view to locate
+        :raise NetAppDriverException: if the snapshot view cannot be
+        located for the snapshot identified by snap_id
+        :return snapshot view for snapshot identified by snap_id
+        """
+        path = self.RESOURCE_PATHS.get('cgroup_cgsnap_views')
+
+        data = {
+            'name': name,
+            'accessMode': 'readOnly',
+            # Only define a view for this snapshot
+            'pitId': snap_id,
+        }
+        # Define a view for the cgsnapshot
+        cgsnapshot_view = self._invoke(
+            'POST', path, data, **{'object-id': cg_id})
+
+        # Retrieve the snapshot views associated with our cgsnapshot view
+        views = self.list_cg_snapshot_views(cg_id, cgsnapshot_view[
+            'cgViewRef'])
+        # Find the snapshot view defined for our snapshot
+        for view in views:
+            if view['basePIT'] == snap_id:
+                return view
+        else:
+            try:
+                self.delete_cg_snapshot_view(cg_id, cgsnapshot_view['id'])
+            finally:
+                raise exception.NetAppDriverException(
+                    'Unable to create snapshot view.')
+
+    def list_cg_snapshot_views(self, cg_id, view_id):
+        path = self.RESOURCE_PATHS.get('cgroup_snapshot_views')
+
+        return self._invoke('GET', path, **{'object-id': cg_id,
+                                            'view-id': view_id})
+
+    def delete_cg_snapshot_view(self, cg_id, view_id):
+        path = self.RESOURCE_PATHS.get('cgroup_snap_view')
+
+        return self._invoke('DELETE', path, **{'object-id': cg_id,
+                                               'view-id': view_id})
+
     def get_pool_operation_progress(self, object_id):
         """Retrieve the progress long-running operations on a storage pool
 
index 1efc87411088c91cf609ff0d9a4fdd0c3e737b60..39319cf1cf5f0b9f06bb7ab17aefb356063caa8d 100644 (file)
@@ -30,7 +30,8 @@ class NetAppEseriesFibreChannelDriver(driver.BaseVD,
                                       driver.ManageableVD,
                                       driver.ExtendVD,
                                       driver.TransferVD,
-                                      driver.SnapshotVD):
+                                      driver.SnapshotVD,
+                                      driver.ConsistencyGroupVD):
     """NetApp E-Series FibreChannel volume driver."""
 
     DRIVER_NAME = 'NetApp_FibreChannel_ESeries'
@@ -100,3 +101,26 @@ class NetAppEseriesFibreChannelDriver(driver.BaseVD,
 
     def get_pool(self, volume):
         return self.library.get_pool(volume)
+
+    def create_cgsnapshot(self, context, cgsnapshot, snapshots):
+        return self.library.create_cgsnapshot(cgsnapshot, snapshots)
+
+    def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
+        return self.library.delete_cgsnapshot(cgsnapshot, snapshots)
+
+    def create_consistencygroup(self, context, group):
+        return self.library.create_consistencygroup(group)
+
+    def delete_consistencygroup(self, context, group, volumes):
+        return self.library.delete_consistencygroup(group, volumes)
+
+    def update_consistencygroup(self, context, group,
+                                add_volumes=None, remove_volumes=None):
+        return self.library.update_consistencygroup(
+            group, add_volumes, remove_volumes)
+
+    def create_consistencygroup_from_src(self, context, group, volumes,
+                                         cgsnapshot=None, snapshots=None,
+                                         source_cg=None, source_vols=None):
+        return self.library.create_consistencygroup_from_src(
+            group, volumes, cgsnapshot, snapshots, source_cg, source_vols)
index eee4a5d01438fc4e4f88601f394a4ddee1795b4f..dcb66a43977ec51975fc5252cb75897f8814b210 100644 (file)
@@ -32,7 +32,8 @@ class NetAppEseriesISCSIDriver(driver.BaseVD,
                                driver.ManageableVD,
                                driver.ExtendVD,
                                driver.TransferVD,
-                               driver.SnapshotVD):
+                               driver.SnapshotVD,
+                               driver.ConsistencyGroupVD):
     """NetApp E-Series iSCSI volume driver."""
 
     DRIVER_NAME = 'NetApp_iSCSI_ESeries'
@@ -100,3 +101,26 @@ class NetAppEseriesISCSIDriver(driver.BaseVD,
 
     def get_pool(self, volume):
         return self.library.get_pool(volume)
+
+    def create_cgsnapshot(self, context, cgsnapshot, snapshots):
+        return self.library.create_cgsnapshot(cgsnapshot, snapshots)
+
+    def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
+        return self.library.delete_cgsnapshot(cgsnapshot, snapshots)
+
+    def create_consistencygroup(self, context, group):
+        return self.library.create_consistencygroup(group)
+
+    def delete_consistencygroup(self, context, group, volumes):
+        return self.library.delete_consistencygroup(group, volumes)
+
+    def update_consistencygroup(self, context, group,
+                                add_volumes=None, remove_volumes=None):
+        return self.library.update_consistencygroup(
+            group, add_volumes, remove_volumes)
+
+    def create_consistencygroup_from_src(self, context, group, volumes,
+                                         cgsnapshot=None, snapshots=None,
+                                         source_cg=None, source_vols=None):
+        return self.library.create_consistencygroup_from_src(
+            group, volumes, cgsnapshot, snapshots, source_cg, source_vols)
index dc3e78f938927a2f87a18d81065f64f576bab564..90cd85dc60f2a20dcb2407fa7997e834a157439d 100644 (file)
@@ -638,12 +638,10 @@ class NetAppESeriesLibrary(object):
         size = volume['size']
 
         dst_vol = self._schedule_and_create_volume(label, size)
+        src_vol = None
         try:
-            src_vol = None
-            src_vol = self._client.create_snapshot_volume(
-                image['id'], utils.convert_uuid_to_es_fmt(
-                    uuid.uuid4()), image['baseVol'])
-            self._copy_volume_high_prior_readonly(src_vol, dst_vol)
+            src_vol = self._create_snapshot_volume(image)
+            self._copy_volume_high_priority_readonly(src_vol, dst_vol)
             LOG.info(_LI("Created volume with label %s."), label)
         except exception.NetAppDriverException:
             with excutils.save_and_reraise_exception():
@@ -656,7 +654,8 @@ class NetAppESeriesLibrary(object):
                     LOG.error(_LE("Failure restarting snap vol. Error: %s."),
                               e)
             else:
-                LOG.warning(_LW("Snapshot volume not found."))
+                LOG.warning(_LW("Snapshot volume creation failed for "
+                                "snapshot %s."), image['id'])
 
         return dst_vol
 
@@ -666,20 +665,19 @@ class NetAppESeriesLibrary(object):
         cinder_utils.synchronized(snapshot['id'])(
             self._create_volume_from_snapshot)(volume, es_snapshot)
 
-    def _copy_volume_high_prior_readonly(self, src_vol, dst_vol):
+    def _copy_volume_high_priority_readonly(self, src_vol, dst_vol):
         """Copies src volume to dest volume."""
         LOG.info(_LI("Copying src vol %(src)s to dest vol %(dst)s."),
                  {'src': src_vol['label'], 'dst': dst_vol['label']})
+        job = None
         try:
-            job = None
-            job = self._client.create_volume_copy_job(src_vol['id'],
-                                                      dst_vol['volumeRef'])
-            while True:
+            job = self._client.create_volume_copy_job(
+                src_vol['id'], dst_vol['volumeRef'])
+
+            def wait_for_copy():
                 j_st = self._client.list_vol_copy_job(job['volcopyRef'])
-                if (j_st['status'] == 'inProgress' or j_st['status'] ==
-                        'pending' or j_st['status'] == 'unknown'):
-                    time.sleep(self.SLEEP_SECS)
-                    continue
+                if (j_st['status'] in ['inProgress', 'pending', 'unknown']):
+                    return
                 if j_st['status'] == 'failed' or j_st['status'] == 'halted':
                     LOG.error(_LE("Vol copy job status %s."), j_st['status'])
                     raise exception.NetAppDriverException(
@@ -687,7 +685,12 @@ class NetAppESeriesLibrary(object):
                         dst_vol['label'])
                 LOG.info(_LI("Vol copy job completed for dest %s."),
                          dst_vol['label'])
-                break
+                raise loopingcall.LoopingCallDone()
+
+            checker = loopingcall.FixedIntervalLoopingCall(wait_for_copy)
+            checker.start(interval=self.SLEEP_SECS,
+                          initial_delay=self.SLEEP_SECS,
+                          stop_on_exception=True).wait()
         finally:
             if job:
                 try:
@@ -702,13 +705,9 @@ class NetAppESeriesLibrary(object):
 
     def create_cloned_volume(self, volume, src_vref):
         """Creates a clone of the specified volume."""
-        snapshot = {'id': uuid.uuid4(), 'volume_id': src_vref['id'],
-                    'volume': src_vref}
-        group_name = (utils.convert_uuid_to_es_fmt(snapshot['id']) +
-                      self.SNAPSHOT_VOL_COPY_SUFFIX)
         es_vol = self._get_volume(src_vref['id'])
 
-        es_snapshot = self._create_es_snapshot(es_vol, group_name)
+        es_snapshot = self._create_es_snapshot_for_clone(es_vol)
 
         try:
             self._create_volume_from_snapshot(volume, es_snapshot)
@@ -717,7 +716,7 @@ class NetAppESeriesLibrary(object):
                 self._client.delete_snapshot_group(es_snapshot['pitGroupRef'])
             except exception.NetAppDriverException:
                 LOG.warning(_LW("Failure deleting temp snapshot %s."),
-                            snapshot['id'])
+                            es_snapshot['id'])
 
     def delete_volume(self, volume):
         """Deletes a volume."""
@@ -728,15 +727,27 @@ class NetAppESeriesLibrary(object):
             LOG.warning(_LW("Volume %s already deleted."), volume['id'])
             return
 
-    def _create_snapshot_volume(self, snapshot_id, label=None):
+    def _is_cgsnapshot(self, snapshot_image):
+        """Determine if an E-Series snapshot image is part of a cgsnapshot"""
+        cg_id = snapshot_image.get('consistencyGroupId')
+        # A snapshot that is not part of a consistency group may have a
+        # cg_id of either none or a string of all 0's, so we check for both
+        return not (cg_id is None or utils.NULL_REF == cg_id)
+
+    def _create_snapshot_volume(self, image):
         """Creates snapshot volume for given group with snapshot_id."""
-        image = self._get_snapshot(snapshot_id)
         group = self._get_snapshot_group(image['pitGroupRef'])
+
         LOG.debug("Creating snap vol for group %s", group['label'])
-        if label is None:
-            label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
-        return self._client.create_snapshot_volume(image['pitRef'], label,
-                                                   image['baseVol'])
+
+        label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
+
+        if self._is_cgsnapshot(image):
+            return self._client.create_cg_snapshot_view(
+                image['consistencyGroupId'], label, image['id'])
+        else:
+            return self._client.create_snapshot_volume(
+                image['pitRef'], label, image['baseVol'])
 
     def _create_snapshot_group(self, label, volume, percentage_capacity=20.0):
         """Define a new snapshot group for a volume
@@ -802,6 +813,9 @@ class NetAppESeriesLibrary(object):
         groups = filter(lambda g: self.SNAPSHOT_VOL_COPY_SUFFIX not in g[
             'label'], groups_for_v)
 
+        # Filter out groups that are part of a consistency group
+        groups = filter(lambda g: not g['consistencyGroup'], groups)
+
         # Find all groups with free snapshot capacity
         groups = [group for group in groups if group.get('snapshotCount') <
                   self.MAX_SNAPSHOT_COUNT]
@@ -833,6 +847,11 @@ class NetAppESeriesLibrary(object):
         else:
             return None
 
+    def _create_es_snapshot_for_clone(self, vol):
+        group_name = (utils.convert_uuid_to_es_fmt(uuid.uuid4()) +
+                      self.SNAPSHOT_VOL_COPY_SUFFIX)
+        return self._create_es_snapshot(vol, group_name)
+
     def _create_es_snapshot(self, vol, group_name=None):
         snap_grp, snap_image = None, None
         try:
@@ -1520,6 +1539,8 @@ class NetAppESeriesLibrary(object):
 
             pool_ssc_info = ssc_stats[poolId]
 
+            pool_ssc_info['consistencygroup_support'] = True
+
             pool_ssc_info[self.ENCRYPTION_UQ_SPEC] = (
                 six.text_type(pool['encrypted']).lower())
 
@@ -1724,6 +1745,280 @@ class NetAppESeriesLibrary(object):
                           initial_delay=self.SLEEP_SECS,
                           stop_on_exception=True).wait()
 
+    def create_cgsnapshot(self, cgsnapshot, snapshots):
+        """Creates a cgsnapshot."""
+        cg_id = cgsnapshot['consistencygroup_id']
+        cg_name = utils.convert_uuid_to_es_fmt(cg_id)
+
+        # Retrieve the E-Series consistency group
+        es_cg = self._get_consistencygroup_by_name(cg_name)
+
+        # Define an E-Series CG Snapshot
+        es_snaphots = self._client.create_consistency_group_snapshot(
+            es_cg['id'])
+
+        # Build the snapshot updates
+        snapshot_updates = list()
+        for snap in snapshots:
+            es_vol = self._get_volume(snap['volume']['id'])
+            for es_snap in es_snaphots:
+                if es_snap['baseVol'] == es_vol['id']:
+                    snapshot_updates.append({
+                        'id': snap['id'],
+                        # Directly track the backend snapshot ID
+                        'provider_id': es_snap['id'],
+                        'status': 'available'
+                    })
+
+        return None, snapshot_updates
+
+    def delete_cgsnapshot(self, cgsnapshot, snapshots):
+        """Deletes a cgsnapshot."""
+
+        cg_id = cgsnapshot['consistencygroup_id']
+        cg_name = utils.convert_uuid_to_es_fmt(cg_id)
+
+        # Retrieve the E-Series consistency group
+        es_cg = self._get_consistencygroup_by_name(cg_name)
+
+        # Find the smallest sequence number defined on the group
+        min_seq_num = min(es_cg['uniqueSequenceNumber'])
+
+        es_snapshots = self._client.get_consistency_group_snapshots(
+            es_cg['id'])
+        es_snap_ids = set(snap.get('provider_id') for snap in snapshots)
+
+        # We need to find a single snapshot that is a part of the CG snap
+        seq_num = None
+        for snap in es_snapshots:
+            if snap['id'] in es_snap_ids:
+                seq_num = snap['pitSequenceNumber']
+                break
+
+        if seq_num is None:
+            raise exception.CgSnapshotNotFound(cgsnapshot_id=cg_id)
+
+        # Perform a full backend deletion of the cgsnapshot
+        if int(seq_num) <= int(min_seq_num):
+            self._client.delete_consistency_group_snapshot(
+                es_cg['id'], seq_num)
+            return None, None
+        else:
+            # Perform a soft-delete, removing this snapshot from cinder
+            # management, and marking it as available for deletion.
+            return cinder_utils.synchronized(cg_id)(
+                self._soft_delete_cgsnapshot)(
+                es_cg, seq_num)
+
+    def _soft_delete_cgsnapshot(self, es_cg, snap_seq_num):
+        """Mark a cgsnapshot as available for deletion from the backend.
+
+        E-Series snapshots cannot be deleted out of order, as older
+        snapshots in the snapshot group are dependent on the newer
+        snapshots. A "soft delete" results in the cgsnapshot being removed
+        from Cinder management, with the snapshot marked as available for
+        deletion once all snapshots dependent on it are also deleted.
+
+        :param es_cg: E-Series consistency group
+        :param snap_seq_num: unique sequence number of the cgsnapshot
+        :return an update to the snapshot index
+        """
+
+        index = self._get_soft_delete_map()
+        cg_ref = es_cg['id']
+        if cg_ref in index:
+            bitset = na_utils.BitSet(int((index[cg_ref])))
+        else:
+            bitset = na_utils.BitSet(0)
+
+        seq_nums = (
+            set([snap['pitSequenceNumber'] for snap in
+                 self._client.get_consistency_group_snapshots(cg_ref)]))
+
+        # Determine the relative index of the snapshot's sequence number
+        for i, seq_num in enumerate(sorted(seq_nums)):
+            if snap_seq_num == seq_num:
+                bitset.set(i)
+                break
+
+        index_update = (
+            self._cleanup_cg_snapshots(cg_ref, seq_nums, bitset))
+
+        self._merge_soft_delete_changes(index_update, None)
+
+        return None, None
+
+    def _cleanup_cg_snapshots(self, cg_ref, seq_nums, bitset):
+        """Delete cg snapshot images that are marked for removal
+
+        The snapshot index tracks all snapshots that have been removed from
+        Cinder, and are therefore available for deletion when this operation
+        is possible.
+
+        CG snapshots are tracked by unique sequence numbers that are
+        associated with 1 or more snapshot images. The sequence numbers are
+        tracked (relative to the 32 images allowed per group), within the
+        snapshot index.
+
+        This method will purge CG snapshots that have been marked as
+        available for deletion within the backend persistent store.
+
+        :param cg_ref: reference to an E-Series consistent group
+        :param seq_nums: set of unique sequence numbers associated with the
+        consistency group
+        :param bitset: the bitset representing which sequence numbers are
+        marked for deletion
+        :return update for the snapshot index
+        """
+        deleted = 0
+        # Order by their sequence number, from oldest to newest
+        for i, seq_num in enumerate(sorted(seq_nums)):
+            if bitset.is_set(i):
+                self._client.delete_consistency_group_snapshot(cg_ref,
+                                                               seq_num)
+                deleted += 1
+            else:
+                # Snapshots must be deleted in order, so if the current
+                # snapshot is not pending deletion, we don't want to
+                # process any more
+                break
+
+        if deleted:
+            # We need to update the bitset to reflect the fact that older
+            # snapshots have been deleted, so snapshot relative indexes
+            # have now been updated.
+            bitset >>= deleted
+
+            LOG.debug('Deleted %(count)s snapshot images from '
+                      'consistency group: %(grp)s.', {'count': deleted,
+                                                      'grp': cg_ref})
+        # Update the index
+        return {cg_ref: repr(bitset)}
+
+    def create_consistencygroup(self, cinder_cg):
+        """Define a consistency group."""
+        self._create_consistency_group(cinder_cg)
+
+        return {'status': 'available'}
+
+    def _create_consistency_group(self, cinder_cg):
+        """Define a new consistency group on the E-Series backend"""
+        name = utils.convert_uuid_to_es_fmt(cinder_cg['id'])
+        return self._client.create_consistency_group(name)
+
+    def _get_consistencygroup(self, cinder_cg):
+        """Retrieve an E-Series consistency group"""
+        name = utils.convert_uuid_to_es_fmt(cinder_cg['id'])
+        return self._get_consistencygroup_by_name(name)
+
+    def _get_consistencygroup_by_name(self, name):
+        """Retrieve an E-Series consistency group by name"""
+
+        for cg in self._client.list_consistency_groups():
+            if name == cg['name']:
+                return cg
+
+        raise exception.ConsistencyGroupNotFound(consistencygroup_id=name)
+
+    def delete_consistencygroup(self, group, volumes):
+        """Deletes a consistency group."""
+
+        volume_update = list()
+
+        for volume in volumes:
+            LOG.info(_LI('Deleting volume %s.'), volume['id'])
+            volume_update.append({
+                'status': 'deleted', 'id': volume['id'],
+            })
+            self.delete_volume(volume)
+
+        try:
+            cg = self._get_consistencygroup(group)
+        except exception.ConsistencyGroupNotFound:
+            LOG.warning(_LW('Consistency group already deleted.'))
+        else:
+            self._client.delete_consistency_group(cg['id'])
+            try:
+                self._merge_soft_delete_changes(None, [cg['id']])
+            except (exception.NetAppDriverException,
+                    eseries_exc.WebServiceException):
+                LOG.warning(_LW('Unable to remove CG from the deletion map.'))
+
+        model_update = {'status': 'deleted'}
+
+        return model_update, volume_update
+
+    def _update_consistency_group_members(self, es_cg,
+                                          add_volumes, remove_volumes):
+        """Add or remove consistency group members
+
+        :param es_cg: The E-Series consistency group
+        :param add_volumes: A list of Cinder volumes to add to the
+        consistency group
+        :param remove_volumes: A list of Cinder volumes to remove from the
+        consistency group
+        :return None
+        """
+        for volume in remove_volumes:
+            es_vol = self._get_volume(volume['id'])
+            LOG.info(
+                _LI('Removing volume %(v)s from consistency group %(''cg)s.'),
+                {'v': es_vol['label'], 'cg': es_cg['label']})
+            self._client.remove_consistency_group_member(es_vol['id'],
+                                                         es_cg['id'])
+
+        for volume in add_volumes:
+            es_vol = self._get_volume(volume['id'])
+            LOG.info(_LI('Adding volume %(v)s to consistency group %(cg)s.'),
+                     {'v': es_vol['label'], 'cg': es_cg['label']})
+            self._client.add_consistency_group_member(
+                es_vol['id'], es_cg['id'])
+
+    def update_consistencygroup(self, group,
+                                add_volumes, remove_volumes):
+        """Add or remove volumes from an existing consistency group"""
+        cg = self._get_consistencygroup(group)
+
+        self._update_consistency_group_members(
+            cg, add_volumes, remove_volumes)
+
+        return None, None, None
+
+    def create_consistencygroup_from_src(self, group, volumes,
+                                         cgsnapshot, snapshots,
+                                         source_cg, source_vols):
+        """Define a consistency group based on an existing group
+
+        Define a new consistency group from a source consistency group. If
+        only a source_cg is provided, then clone each base volume and add
+        it to a new consistency group. If a cgsnapshot is provided,
+        clone each snapshot image to a new volume and add it to the cg.
+
+        :param group: The new consistency group to define
+        :param volumes: The volumes to add to the consistency group
+        :param cgsnapshot: The cgsnapshot to base the group on
+        :param snapshots: The list of snapshots on the source cg
+        :param source_cg: The source consistency group
+        :param source_vols: The volumes added to the source cg
+        """
+        cg = self._create_consistency_group(group)
+        if cgsnapshot:
+            for vol, snap in zip(volumes, snapshots):
+                image = self._get_snapshot(snap)
+                self._create_volume_from_snapshot(vol, image)
+        else:
+            for vol, src in zip(volumes, source_vols):
+                es_vol = self._get_volume(src['id'])
+                es_snapshot = self._create_es_snapshot_for_clone(es_vol)
+                try:
+                    self._create_volume_from_snapshot(vol, es_snapshot)
+                finally:
+                    self._delete_es_snapshot(es_snapshot)
+
+        self._update_consistency_group_members(cg, volumes, [])
+
+        return None, None
+
     def _garbage_collect_tmp_vols(self):
         """Removes tmp vols with no snapshots."""
         try:
diff --git a/releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml b/releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml
new file mode 100644 (file)
index 0000000..6a3bfb5
--- /dev/null
@@ -0,0 +1,3 @@
+---
+features:
+  - Support for Consistency Groups in the NetApp E-Series Volume Driver