]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Dell SC: Add support for consistency groups
authortswanson <tswanson@compellent.com>
Mon, 29 Jun 2015 19:21:45 +0000 (14:21 -0500)
committerTom Swanson <tom_swanson@dell.com>
Mon, 6 Jul 2015 19:12:23 +0000 (14:12 -0500)
Added support for consistency groups. Associated tests added to
test_dellsc.py and test_dellscapi.py. create_consistency_group_from_src
is not implemented in this patch.

Minor change to iscsi driver inheritance.

Change-Id: I485329883ba35b341f1b3e79897e5fe6680de1f3
Implements: blueprint dell-add-consistency-groups

cinder/tests/unit/test_dellsc.py
cinder/tests/unit/test_dellscapi.py
cinder/volume/drivers/dell/dell_storagecenter_api.py
cinder/volume/drivers/dell/dell_storagecenter_common.py
cinder/volume/drivers/dell/dell_storagecenter_fc.py
cinder/volume/drivers/dell/dell_storagecenter_iscsi.py

index 95bc8c674a398640f5292928c3229ccec4f0c064..a99fdd60b5b29de20906055d2e26fd3f23e55a0f 100644 (file)
@@ -176,6 +176,21 @@ class DellSCSanISCSIDriverTestCase(test.TestCase):
              u'size': u'0.0 Bytes'
              }
 
+    SCRPLAYPROFILE = {u'ruleCount': 0,
+                      u'name': u'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                      u'volumeCount': 0,
+                      u'scName': u'Storage Center 64702',
+                      u'notes': u'Created by Dell Cinder Driver',
+                      u'scSerialNumber': 64702,
+                      u'userCreated': True,
+                      u'instanceName': u'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                      u'instanceId': u'64702.11',
+                      u'enforceReplayCreationTimeout': False,
+                      u'replayCreationTimeout': 20,
+                      u'objectType': u'ScReplayProfile',
+                      u'type': u'Consistent',
+                      u'expireIncompleteReplaySets': True}
+
     IQN = 'iqn.2002-03.com.compellent:5000D31000000001'
 
     ISCSI_PROPERTIES = {'access_mode': 'rw',
@@ -1117,3 +1132,322 @@ class DellSCSanISCSIDriverTestCase(test.TestCase):
         mock_find_sc.assert_called_once_with()
         mock_find_volume.assert_called_once_with(None)
         self.assertEqual({'_name_id': None}, rt)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'create_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_create_consistencygroup(self,
+                                     mock_create_replay_profile,
+                                     mock_close_connection,
+                                     mock_open_connection,
+                                     mock_init):
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3'}
+        self.driver.create_consistencygroup(context, group)
+        mock_create_replay_profile.assert_called_once_with(group['id'])
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'create_replay_profile',
+                       return_value=None)
+    def test_create_consistencygroup_fail(self,
+                                          mock_create_replay_profile,
+                                          mock_close_connection,
+                                          mock_open_connection,
+                                          mock_init):
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3'}
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_consistencygroup, context, group)
+        mock_create_replay_profile.assert_called_once_with(group['id'])
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'delete_replay_profile')
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    @mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
+                       'delete_volume')
+    def test_delete_consistencygroup(self,
+                                     mock_delete_volume,
+                                     mock_find_replay_profile,
+                                     mock_delete_replay_profile,
+                                     mock_close_connection,
+                                     mock_open_connection,
+                                     mock_init):
+        self.driver.db = mock.Mock()
+        mock_volume = mock.MagicMock()
+        expected_volumes = [mock_volume]
+        self.driver.db.volume_get_all_by_group.return_value = expected_volumes
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                 'status': 'deleted'}
+        model_update, volumes = self.driver.delete_consistencygroup(context,
+                                                                    group)
+        mock_find_replay_profile.assert_called_once_with(group['id'])
+        mock_delete_replay_profile.assert_called_once_with(self.SCRPLAYPROFILE)
+        mock_delete_volume.assert_called_once_with(mock_volume)
+        self.assertEqual(group['status'], model_update['status'])
+        self.assertEqual(expected_volumes, volumes)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'delete_replay_profile')
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=None)
+    @mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
+                       'delete_volume')
+    def test_delete_consistencygroup_not_found(self,
+                                               mock_delete_volume,
+                                               mock_find_replay_profile,
+                                               mock_delete_replay_profile,
+                                               mock_close_connection,
+                                               mock_open_connection,
+                                               mock_init):
+        self.driver.db = mock.Mock()
+        mock_volume = mock.MagicMock()
+        expected_volumes = [mock_volume]
+        self.driver.db.volume_get_all_by_group.return_value = expected_volumes
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                 'status': 'deleted'}
+        model_update, volumes = self.driver.delete_consistencygroup(context,
+                                                                    group)
+        mock_find_replay_profile.assert_called_once_with(group['id'])
+        self.assertFalse(mock_delete_replay_profile.called)
+        mock_delete_volume.assert_called_once_with(mock_volume)
+        self.assertEqual(group['status'], model_update['status'])
+        self.assertEqual(expected_volumes, volumes)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'update_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_update_consistencygroup(self,
+                                     mock_find_replay_profile,
+                                     mock_update_cg_volumes,
+                                     mock_close_connection,
+                                     mock_open_connection,
+                                     mock_init):
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3'}
+        add_volumes = [{'id': '101'}]
+        remove_volumes = [{'id': '102'}]
+        rt1, rt2, rt3 = self.driver.update_consistencygroup(context,
+                                                            group,
+                                                            add_volumes,
+                                                            remove_volumes)
+        mock_update_cg_volumes.assert_called_once_with(self.SCRPLAYPROFILE,
+                                                       add_volumes,
+                                                       remove_volumes)
+        mock_find_replay_profile.assert_called_once_with(group['id'])
+        self.assertIsNone(rt1)
+        self.assertIsNone(rt2)
+        self.assertIsNone(rt3)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=None)
+    def test_update_consistencygroup_not_found(self,
+                                               mock_find_replay_profile,
+                                               mock_close_connection,
+                                               mock_open_connection,
+                                               mock_init):
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3'}
+        add_volumes = [{'id': '101'}]
+        remove_volumes = [{'id': '102'}]
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.update_consistencygroup,
+                          context,
+                          group,
+                          add_volumes,
+                          remove_volumes)
+        mock_find_replay_profile.assert_called_once_with(group['id'])
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'update_cg_volumes',
+                       return_value=False)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_update_consistencygroup_error(self,
+                                           mock_find_replay_profile,
+                                           mock_update_cg_volumes,
+                                           mock_close_connection,
+                                           mock_open_connection,
+                                           mock_init):
+        context = {}
+        group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3'}
+        add_volumes = [{'id': '101'}]
+        remove_volumes = [{'id': '102'}]
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.update_consistencygroup,
+                          context,
+                          group,
+                          add_volumes,
+                          remove_volumes)
+        mock_find_replay_profile.assert_called_once_with(group['id'])
+        mock_update_cg_volumes.assert_called_once_with(self.SCRPLAYPROFILE,
+                                                       add_volumes,
+                                                       remove_volumes)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'snap_cg_replay',
+                       return_value={'instanceId': '100'})
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_create_cgsnapshot(self,
+                               mock_find_replay_profile,
+                               mock_snap_cg_replay,
+                               mock_close_connection,
+                               mock_open_connection,
+                               mock_init):
+        self.driver.db = mock.Mock()
+        mock_snapshot = mock.MagicMock()
+        expected_snapshots = [mock_snapshot]
+        self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = (
+            expected_snapshots)
+        context = {}
+        cggrp = {'consistencygroup_id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                 'id': '100'}
+        model_update, snapshots = self.driver.create_cgsnapshot(context, cggrp)
+        mock_find_replay_profile.assert_called_once_with(
+            cggrp['consistencygroup_id'])
+        mock_snap_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE,
+                                                    cggrp['id'],
+                                                    0)
+        self.assertEqual('available', model_update['status'])
+        self.assertEqual(expected_snapshots, snapshots)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=None)
+    def test_create_cgsnapshot_profile_not_found(self,
+                                                 mock_find_replay_profile,
+                                                 mock_close_connection,
+                                                 mock_open_connection,
+                                                 mock_init):
+        context = {}
+        cggrp = {'consistencygroup_id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                 'id': '100'}
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_cgsnapshot,
+                          context,
+                          cggrp)
+        mock_find_replay_profile.assert_called_once_with(
+            cggrp['consistencygroup_id'])
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'snap_cg_replay',
+                       return_value=None)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_create_cgsnapshot_fail(self,
+                                    mock_find_replay_profile,
+                                    mock_snap_cg_replay,
+                                    mock_close_connection,
+                                    mock_open_connection,
+                                    mock_init):
+        context = {}
+        cggrp = {'consistencygroup_id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                 'id': '100'}
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_cgsnapshot,
+                          context,
+                          cggrp)
+        mock_find_replay_profile.assert_called_once_with(
+            cggrp['consistencygroup_id'])
+        mock_snap_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE,
+                                                    cggrp['id'],
+                                                    0)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'delete_cg_replay',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_delete_cgsnapshot(self,
+                               mock_find_replay_profile,
+                               mock_delete_cg_replay,
+                               mock_close_connection,
+                               mock_open_connection,
+                               mock_init):
+        self.driver.db = mock.Mock()
+        mock_snapshot = mock.MagicMock()
+        expected_snapshots = [mock_snapshot]
+        self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = (
+            expected_snapshots)
+        context = {}
+        cgsnap = {'consistencygroup_id':
+                  'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                  'id': '100',
+                  'status': 'deleted'}
+        model_update, snapshots = self.driver.delete_cgsnapshot(context,
+                                                                cgsnap)
+        mock_find_replay_profile.assert_called_once_with(
+            cgsnap['consistencygroup_id'])
+        mock_delete_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE,
+                                                      cgsnap['id'])
+        self.assertEqual({'status': cgsnap['status']}, model_update)
+        self.assertEqual(expected_snapshots, snapshots)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'delete_cg_replay')
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=None)
+    def test_delete_cgsnapshot_profile_not_found(self,
+                                                 mock_find_replay_profile,
+                                                 mock_delete_cg_replay,
+                                                 mock_close_connection,
+                                                 mock_open_connection,
+                                                 mock_init):
+        self.driver.db = mock.Mock()
+        mock_snapshot = mock.MagicMock()
+        expected_snapshots = [mock_snapshot]
+        self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = (
+            expected_snapshots)
+        context = {}
+        cgsnap = {'consistencygroup_id':
+                  'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                  'id': '100',
+                  'status': 'deleted'}
+        model_update, snapshots = self.driver.delete_cgsnapshot(context,
+                                                                cgsnap)
+        mock_find_replay_profile.assert_called_once_with(
+            cgsnap['consistencygroup_id'])
+
+        self.assertFalse(mock_delete_cg_replay.called)
+        self.assertEqual({'status': cgsnap['status']}, model_update)
+        self.assertEqual(expected_snapshots, snapshots)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'delete_cg_replay',
+                       return_value=False)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=SCRPLAYPROFILE)
+    def test_delete_cgsnapshot_profile_failed_delete(self,
+                                                     mock_find_replay_profile,
+                                                     mock_delete_cg_replay,
+                                                     mock_close_connection,
+                                                     mock_open_connection,
+                                                     mock_init):
+        context = {}
+        cgsnap = {'consistencygroup_id':
+                  'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                  'id': '100',
+                  'status': 'available'}
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.delete_cgsnapshot,
+                          context,
+                          cgsnap)
+        mock_find_replay_profile.assert_called_once_with(
+            cgsnap['consistencygroup_id'])
+        mock_delete_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE,
+                                                      cgsnap['id'])
index aaa1ee2f38dd6a321f3a2ee2057a53600dacf802..6fc071a4507825e3a1b32f1bbbc46e0ff83a21d5 100644 (file)
@@ -1391,6 +1391,20 @@ class DellSCSanAPITestCase(test.TestCase):
                   u'storageAlertThreshold': 10,
                   u'objectType': u'StorageCenterStorageUsage'}
 
+    RPLAY_PROFILE = {u'name': u'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
+                     u'type': u'Consistent',
+                     u'notes': u'Created by Dell Cinder Driver',
+                     u'volumeCount': 0,
+                     u'expireIncompleteReplaySets': True,
+                     u'replayCreationTimeout': 20,
+                     u'enforceReplayCreationTimeout': False,
+                     u'ruleCount': 0,
+                     u'userCreated': True,
+                     u'scSerialNumber': 64702,
+                     u'scName': u'Storage Center 64702',
+                     u'objectType': u'ScReplayProfile',
+                     u'instanceId': u'64702.11',
+                     u'instanceName': u'fc8f2fec-fab2-4e34-9148-c094c913b9a3'}
     STORAGE_PROFILE_LIST = [
         {u'allowedForFlashOptimized': False,
          u'allowedForNonFlashOptimized': True,
@@ -1489,12 +1503,19 @@ class DellSCSanAPITestCase(test.TestCase):
     response_created.reason = u'created'
     RESPONSE_201 = response_created
 
-    # Create a Response object that indicates a failure (no content)
+    # Create a Response object that can indicate a failure. Although
+    # 204 can be a success with no return.  (Know your calls!)
     response_nc = models.Response()
     response_nc.status_code = 204
     response_nc.reason = u'duplicate'
     RESPONSE_204 = response_nc
 
+    # Create a Response object is a pure error.
+    response_bad = models.Response()
+    response_bad.status_code = 400
+    response_bad.reason = u'bad request'
+    RESPONSE_400 = response_bad
+
     def setUp(self):
         super(DellSCSanAPITestCase, self).setUp()
 
@@ -3994,6 +4015,680 @@ class DellSCSanAPITestCase(test.TestCase):
         self.assertFalse(mock_delete.called)
         self.assertIsNone(res, 'Expected None')
 
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_json',
+                       return_value=[RPLAY_PROFILE])
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_200)
+    def test_find_replay_profile(self,
+                                 mock_post,
+                                 mock_get_json,
+                                 mock_close_connection,
+                                 mock_open_connection,
+                                 mock_init):
+        res = self.scapi.find_replay_profile('guid')
+        self.assertTrue(mock_post.called)
+        self.assertTrue(mock_get_json.called)
+        self.assertEqual(self.RPLAY_PROFILE, res, 'Unexpected Profile')
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_json',
+                       return_value=[RPLAY_PROFILE, RPLAY_PROFILE])
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_200)
+    def test_find_replay_profile_more_than_one(self,
+                                               mock_post,
+                                               mock_get_json,
+                                               mock_close_connection,
+                                               mock_open_connection,
+                                               mock_init):
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.scapi.find_replay_profile,
+                          'guid')
+        self.assertTrue(mock_post.called)
+        self.assertTrue(mock_get_json.called)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_json',
+                       return_value=[])
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_200)
+    def test_find_replay_profile_empty_list(self,
+                                            mock_post,
+                                            mock_get_json,
+                                            mock_close_connection,
+                                            mock_open_connection,
+                                            mock_init):
+        res = self.scapi.find_replay_profile('guid')
+        self.assertTrue(mock_post.called)
+        self.assertTrue(mock_get_json.called)
+        self.assertEqual(None, res, 'Unexpected return')
+
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_400)
+    def test_find_replay_profile_error(self,
+                                       mock_post,
+                                       mock_close_connection,
+                                       mock_open_connection,
+                                       mock_init):
+        res = self.scapi.find_replay_profile('guid')
+        self.assertTrue(mock_post.called)
+        self.assertEqual(None, res, 'Unexpected return')
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=None)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_first_result',
+                       return_value=RPLAY_PROFILE)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_201)
+    def test_create_replay_profile(self,
+                                   mock_post,
+                                   mock_first_result,
+                                   mock_find_replay_profile,
+                                   mock_close_connection,
+                                   mock_open_connection,
+                                   mock_init):
+        res = self.scapi.create_replay_profile('guid')
+        self.assertTrue(mock_find_replay_profile.called)
+        self.assertTrue(mock_post.called)
+        self.assertTrue(mock_first_result.called)
+        self.assertEqual(self.RPLAY_PROFILE, res, 'Unexpected Profile')
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=RPLAY_PROFILE)
+    def test_create_replay_profile_exists(self,
+                                          mock_find_replay_profile,
+                                          mock_close_connection,
+                                          mock_open_connection,
+                                          mock_init):
+        res = self.scapi.create_replay_profile('guid')
+        self.assertTrue(mock_find_replay_profile.called)
+        self.assertEqual(self.RPLAY_PROFILE, res, 'Unexpected Profile')
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay_profile',
+                       return_value=None)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_400)
+    def test_create_replay_profile_fail(self,
+                                        mock_post,
+                                        mock_find_replay_profile,
+                                        mock_close_connection,
+                                        mock_open_connection,
+                                        mock_init):
+        res = self.scapi.create_replay_profile('guid')
+        self.assertTrue(mock_find_replay_profile.called)
+        self.assertTrue(mock_post.called)
+        self.assertEqual(None, res, 'Unexpected return')
+
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'delete',
+                       return_value=RESPONSE_200)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id')
+    def test_delete_replay_profile(self,
+                                   mock_get_id,
+                                   mock_delete,
+                                   mock_close_connection,
+                                   mock_open_connection,
+                                   mock_init):
+        profile = {'name': 'guid'}
+        self.scapi.delete_replay_profile(profile)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_delete.called)
+
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'delete',
+                       return_value=RESPONSE_400)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id')
+    def test_delete_replay_profile_fail(self,
+                                        mock_get_id,
+                                        mock_delete,
+                                        mock_close_connection,
+                                        mock_open_connection,
+                                        mock_init):
+        profile = {'name': 'guid'}
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.scapi.delete_replay_profile,
+                          profile)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_delete.called)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_first_result',
+                       return_value=VOLUME_CONFIG)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'get',
+                       return_value=RESPONSE_200)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id')
+    def test_get_volume_configuration(self,
+                                      mock_get_id,
+                                      mock_get,
+                                      mock_first_result,
+                                      mock_close_connection,
+                                      mock_open_connection,
+                                      mock_init):
+        res = self.scapi._get_volume_configuration({})
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get.called)
+        self.assertEqual(self.VOLUME_CONFIG, res, 'Unexpected config')
+
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'get',
+                       return_value=RESPONSE_400)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id')
+    def test_get_volume_configuration_bad_response(self,
+                                                   mock_get_id,
+                                                   mock_get,
+                                                   mock_close_connection,
+                                                   mock_open_connection,
+                                                   mock_init):
+        res = self.scapi._get_volume_configuration({})
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get.called)
+        self.assertEqual(None, res, 'Unexpected result')
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_volume_configuration',
+                       return_value=VOLUME_CONFIG)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'put',
+                       return_value=RESPONSE_200)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id')
+    def test_update_volume_profiles(self,
+                                    mock_get_id,
+                                    mock_put,
+                                    mock_get_volume_configuration,
+                                    mock_close_connection,
+                                    mock_open_connection,
+                                    mock_init):
+        scvolume = {'instanceId': '1'}
+        existingid = self.VOLUME_CONFIG[u'replayProfileList'][0][u'instanceId']
+        vcid = self.VOLUME_CONFIG[u'instanceId']
+        # First get_id is for our existing replay profile id and the second
+        # is for the volume config and the last is for the volume id.  And
+        # then we do this again for the second call below.
+        mock_get_id.side_effect = [existingid,
+                                   vcid,
+                                   scvolume['instanceId'],
+                                   existingid,
+                                   vcid,
+                                   scvolume['instanceId']]
+        newid = '64702.1'
+        expected_payload = {'ReplayProfileList': [newid, existingid]}
+        expected_url = 'StorageCenter/ScVolumeConfiguration/' + vcid
+        res = self.scapi._update_volume_profiles(scvolume, newid, None)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get_volume_configuration.called)
+        mock_put.assert_called_once_with(expected_url,
+                                         expected_payload)
+        self.assertTrue(res)
+
+        # Now do a remove.  (Restarting with the original config so this will
+        # end up as an empty list.)
+        expected_payload['ReplayProfileList'] = []
+        res = self.scapi._update_volume_profiles(scvolume, None, existingid)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get_volume_configuration.called)
+        mock_put.assert_called_with(expected_url,
+                                    expected_payload)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_volume_configuration',
+                       return_value=VOLUME_CONFIG)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'put',
+                       return_value=RESPONSE_400)
+    # We set this to 1 so we can check our payload
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id')
+    def test_update_volume_profiles_bad_response(self,
+                                                 mock_get_id,
+                                                 mock_put,
+                                                 mock_get_volume_configuration,
+                                                 mock_close_connection,
+                                                 mock_open_connection,
+                                                 mock_init):
+        scvolume = {'instanceId': '1'}
+        existingid = self.VOLUME_CONFIG[u'replayProfileList'][0][u'instanceId']
+        vcid = self.VOLUME_CONFIG[u'instanceId']
+        # First get_id is for our existing replay profile id and the second
+        # is for the volume config and the last is for the volume id.  And
+        # then we do this again for the second call below.
+        mock_get_id.side_effect = [existingid,
+                                   vcid,
+                                   scvolume['instanceId'],
+                                   existingid,
+                                   vcid,
+                                   scvolume['instanceId']]
+        newid = '64702.1'
+        expected_payload = {'ReplayProfileList': [newid, existingid]}
+        expected_url = 'StorageCenter/ScVolumeConfiguration/' + vcid
+        res = self.scapi._update_volume_profiles(scvolume, newid, None)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get_volume_configuration.called)
+        mock_put.assert_called_once_with(expected_url,
+                                         expected_payload)
+        self.assertFalse(res)
+
+        # Now do a remove.  (Restarting with the original config so this will
+        # end up as an empty list.)
+        expected_payload['ReplayProfileList'] = []
+        res = self.scapi._update_volume_profiles(scvolume, None, existingid)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get_volume_configuration.called)
+        mock_put.assert_called_with(expected_url,
+                                    expected_payload)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_volume_configuration',
+                       return_value=None)
+    def test_update_volume_profiles_no_config(self,
+                                              mock_get_volume_configuration,
+                                              mock_close_connection,
+                                              mock_open_connection,
+                                              mock_init):
+        scvolume = {'instanceId': '1'}
+        res = self.scapi._update_volume_profiles(scvolume, '64702.2', None)
+        self.assertTrue(mock_get_volume_configuration.called)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_volume',
+                       return_value=999)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_update_volume_profiles',
+                       return_value=True)
+    def test_add_cg_volumes(self,
+                            mock_update_volume_profiles,
+                            mock_find_volume,
+                            mock_close_connection,
+                            mock_open_connection,
+                            mock_init):
+        profileid = '100'
+        add_volumes = [{'id': '1'}]
+        res = self.scapi._add_cg_volumes(profileid, add_volumes)
+        self.assertTrue(mock_find_volume.called)
+        mock_update_volume_profiles.assert_called_once_with(999,
+                                                            addid=profileid,
+                                                            removeid=None)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_volume',
+                       return_value=999)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_update_volume_profiles',
+                       return_value=False)
+    def test_add_cg_volumes_fail(self,
+                                 mock_update_volume_profiles,
+                                 mock_find_volume,
+                                 mock_close_connection,
+                                 mock_open_connection,
+                                 mock_init):
+        profileid = '100'
+        add_volumes = [{'id': '1'}]
+        res = self.scapi._add_cg_volumes(profileid, add_volumes)
+        self.assertTrue(mock_find_volume.called)
+        mock_update_volume_profiles.assert_called_once_with(999,
+                                                            addid=profileid,
+                                                            removeid=None)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_volume',
+                       return_value=999)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_update_volume_profiles',
+                       return_value=True)
+    def test_remove_cg_volumes(self,
+                               mock_update_volume_profiles,
+                               mock_find_volume,
+                               mock_close_connection,
+                               mock_open_connection,
+                               mock_init):
+        profileid = '100'
+        remove_volumes = [{'id': '1'}]
+        res = self.scapi._remove_cg_volumes(profileid, remove_volumes)
+        self.assertTrue(mock_find_volume.called)
+        mock_update_volume_profiles.assert_called_once_with(999,
+                                                            addid=None,
+                                                            removeid=profileid)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_volume',
+                       return_value=999)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_update_volume_profiles',
+                       return_value=False)
+    def test_remove_cg_volumes_false(self,
+                                     mock_update_volume_profiles,
+                                     mock_find_volume,
+                                     mock_close_connection,
+                                     mock_open_connection,
+                                     mock_init):
+        profileid = '100'
+        remove_volumes = [{'id': '1'}]
+        res = self.scapi._remove_cg_volumes(profileid, remove_volumes)
+        self.assertTrue(mock_find_volume.called)
+        mock_update_volume_profiles.assert_called_once_with(999,
+                                                            addid=None,
+                                                            removeid=profileid)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_remove_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_add_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_update_cg_volumes(self,
+                               mock_get_id,
+                               mock_add_cg_volumes,
+                               mock_remove_cg_volumes,
+                               mock_close_connection,
+                               mock_open_connection,
+                               mock_init):
+        profile = {'name': 'guid'}
+        add_volumes = [{'id': '1'}]
+        remove_volumes = [{'id': '2'}]
+        res = self.scapi.update_cg_volumes(profile,
+                                           add_volumes,
+                                           remove_volumes)
+        self.assertTrue(mock_get_id.called)
+        mock_add_cg_volumes.assert_called_once_with('100', add_volumes)
+        mock_remove_cg_volumes.assert_called_once_with('100',
+                                                       remove_volumes)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_remove_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_add_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_update_cg_volumes_no_remove(self,
+                                         mock_get_id,
+                                         mock_add_cg_volumes,
+                                         mock_remove_cg_volumes,
+                                         mock_close_connection,
+                                         mock_open_connection,
+                                         mock_init):
+        profile = {'name': 'guid'}
+        add_volumes = [{'id': '1'}]
+        remove_volumes = []
+        res = self.scapi.update_cg_volumes(profile,
+                                           add_volumes,
+                                           remove_volumes)
+        self.assertTrue(mock_get_id.called)
+        mock_add_cg_volumes.assert_called_once_with('100', add_volumes)
+        self.assertFalse(mock_remove_cg_volumes.called)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_remove_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_add_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_update_cg_volumes_no_add(self,
+                                      mock_get_id,
+                                      mock_add_cg_volumes,
+                                      mock_remove_cg_volumes,
+                                      mock_close_connection,
+                                      mock_open_connection,
+                                      mock_init):
+        profile = {'name': 'guid'}
+        add_volumes = []
+        remove_volumes = [{'id': '1'}]
+        res = self.scapi.update_cg_volumes(profile,
+                                           add_volumes,
+                                           remove_volumes)
+        self.assertTrue(mock_get_id.called)
+        mock_remove_cg_volumes.assert_called_once_with('100', remove_volumes)
+        self.assertFalse(mock_add_cg_volumes.called)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_remove_cg_volumes')
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_add_cg_volumes',
+                       return_value=False)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_update_cg_volumes_add_fail(self,
+                                        mock_get_id,
+                                        mock_add_cg_volumes,
+                                        mock_remove_cg_volumes,
+                                        mock_close_connection,
+                                        mock_open_connection,
+                                        mock_init):
+        profile = {'name': 'guid'}
+        add_volumes = [{'id': '1'}]
+        remove_volumes = [{'id': '2'}]
+        res = self.scapi.update_cg_volumes(profile,
+                                           add_volumes,
+                                           remove_volumes)
+        self.assertTrue(mock_get_id.called)
+        mock_add_cg_volumes.assert_called_once_with('100', add_volumes)
+        self.assertTrue(not mock_remove_cg_volumes.called)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_remove_cg_volumes',
+                       return_value=False)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_add_cg_volumes',
+                       return_value=True)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_update_cg_volumes_remove_fail(self,
+                                           mock_get_id,
+                                           mock_add_cg_volumes,
+                                           mock_remove_cg_volumes,
+                                           mock_close_connection,
+                                           mock_open_connection,
+                                           mock_init):
+        profile = {'name': 'guid'}
+        add_volumes = [{'id': '1'}]
+        remove_volumes = [{'id': '2'}]
+        res = self.scapi.update_cg_volumes(profile,
+                                           add_volumes,
+                                           remove_volumes)
+        self.assertTrue(mock_get_id.called)
+        mock_add_cg_volumes.assert_called_once_with('100', add_volumes)
+        mock_remove_cg_volumes.assert_called_once_with('100',
+                                                       remove_volumes)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_204)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_snap_cg_replay(self,
+                            mock_get_id,
+                            mock_post,
+                            mock_close_connection,
+                            mock_open_connection,
+                            mock_init):
+        replayid = 'guid'
+        expire = 0
+        profile = {'instanceId': '100'}
+        # See the 100 from get_id above?
+        expected_url = 'StorageCenter/ScReplayProfile/100/CreateReplay'
+        expected_payload = {'description': replayid, 'expireTime': expire}
+        res = self.scapi.snap_cg_replay(profile, replayid, expire)
+        mock_post.assert_called_once_with(expected_url, expected_payload)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_400)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_snap_cg_replay_bad_return(self,
+                                       mock_get_id,
+                                       mock_post,
+                                       mock_close_connection,
+                                       mock_open_connection,
+                                       mock_init):
+        replayid = 'guid'
+        expire = 0
+        profile = {'instanceId': '100'}
+        # See the 100 from get_id above?
+        expected_url = 'StorageCenter/ScReplayProfile/100/CreateReplay'
+        expected_payload = {'description': replayid, 'expireTime': expire}
+        res = self.scapi.snap_cg_replay(profile, replayid, expire)
+        mock_post.assert_called_once_with(expected_url, expected_payload)
+        self.assertTrue(mock_get_id.called)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_json',
+                       return_value=RPLAYS)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'get')
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_find_cg_replay(self,
+                            mock_get_id,
+                            mock_get,
+                            mock_get_json,
+                            mock_close_connection,
+                            mock_open_connection,
+                            mock_init):
+        profile = {'instanceId': '100'}
+        replayid = 'Cinder Test Replay012345678910'
+        res = self.scapi.find_cg_replay(profile, replayid)
+        expected_url = 'StorageCenter/ScReplayProfile/100/ReplayList'
+        mock_get.assert_called_once_with(expected_url)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get_json.called)
+        # We should fine RPLAYS[1]
+        self.assertEqual(self.RPLAYS[1], res, 'Wrong replay returned')
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_json',
+                       return_value=None)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'get')
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_find_cg_replay_bad_json(self,
+                                     mock_get_id,
+                                     mock_get,
+                                     mock_get_json,
+                                     mock_close_connection,
+                                     mock_open_connection,
+                                     mock_init):
+        profile = {'instanceId': '100'}
+        replayid = 'Cinder Test Replay012345678910'
+        res = self.scapi.find_cg_replay(profile, replayid)
+        expected_url = 'StorageCenter/ScReplayProfile/100/ReplayList'
+        mock_get.assert_called_once_with(expected_url)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_get_json.called)
+        self.assertIsNone(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay',
+                       return_value=RPLAY)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_204)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_delete_cg_replay(self,
+                              mock_get_id,
+                              mock_post,
+                              mock_find_replay,
+                              mock_close_connection,
+                              mock_open_connection,
+                              mock_init):
+        profile = {'instanceId': '100'}
+        replayid = 'guid'
+        expected_url = 'StorageCenter/ScReplay/100/Expire'
+        expected_payload = {}
+        res = self.scapi.delete_cg_replay(profile, replayid)
+        mock_post.assert_called_once_with(expected_url, expected_payload)
+        self.assertTrue(mock_find_replay.called)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay',
+                       return_value=RPLAY)
+    @mock.patch.object(dell_storagecenter_api.HttpClient,
+                       'post',
+                       return_value=RESPONSE_400)
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       '_get_id',
+                       return_value='100')
+    def test_delete_cg_replay_error(self,
+                                    mock_get_id,
+                                    mock_post,
+                                    mock_find_replay,
+                                    mock_close_connection,
+                                    mock_open_connection,
+                                    mock_init):
+        profile = {'instanceId': '100'}
+        replayid = 'guid'
+        expected_url = 'StorageCenter/ScReplay/100/Expire'
+        expected_payload = {}
+        res = self.scapi.delete_cg_replay(profile, replayid)
+        mock_post.assert_called_once_with(expected_url, expected_payload)
+        self.assertTrue(mock_get_id.called)
+        self.assertTrue(mock_find_replay.called)
+        self.assertFalse(res)
+
+    @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+                       'find_replay',
+                       return_value=None)
+    def test_delete_cg_replay_cant_find(self,
+                                        mock_find_replay,
+                                        mock_close_connection,
+                                        mock_open_connection,
+                                        mock_init):
+        profile = {'instanceId': '100'}
+        replayid = 'guid'
+        res = self.scapi.delete_cg_replay(profile, replayid)
+        self.assertTrue(mock_find_replay.called)
+        self.assertTrue(res)
+
 
 class DellSCSanAPIConnectionTestCase(test.TestCase):
 
index fa85dbb3b11c0d78315513cc9eb616da78238178..ddc38542ba160850a16434c57b2cdfcf21235c54 100644 (file)
@@ -169,7 +169,7 @@ class StorageCenterApi(object):
     Handles calls to Dell Enterprise Manager (EM) via the REST API interface.
     '''
 
-    APIVERSION = '1.0.2'
+    APIVERSION = '1.2.0'
 
     def __init__(self, host, port, user, password, verify):
         '''This creates a connection to Dell Enterprise Manager.
@@ -297,6 +297,7 @@ class StorageCenterApi(object):
         payload['ApplicationVersion'] = self.APIVERSION
         r = self.client.post('ApiConnection/Login',
                              payload)
+        # TODO(Swanson): If we get a 400 back we should also print the text.
         if r.status_code != 200:
             LOG.error(_LE('Login error: %(code)d %(reason)s'),
                       {'code': r.status_code,
@@ -1084,6 +1085,7 @@ class StorageCenterApi(object):
         :returns: Active controller ID.
         '''
         actvctrl = None
+        # TODO(Swanson): We have a function that gets this.  Call that.
         r = self.client.get('StorageCenter/ScVolume/%s/VolumeConfiguration'
                             % self._get_id(scvolume))
         if r.status_code == 200:
@@ -1553,3 +1555,273 @@ class StorageCenterApi(object):
                        'reason': r.reason})
         else:
             LOG.debug('_delete_server: deleteAllowed is False.')
+
+    def find_replay_profile(self, name):
+        '''Finds the Dell SC replay profile object name.
+
+        :param name: Name of the replay profile object. This is the
+                     consistency group id.
+        :return: Dell SC replay profile or None.
+        :raises: VolumeBackendAPIException
+        '''
+        pf = PayloadFilter()
+        pf.append('ScSerialNumber', self.ssn)
+        pf.append('Name', name)
+        r = self.client.post('StorageCenter/ScReplayProfile/GetList',
+                             pf.payload)
+        if r.status_code == 200:
+            profilelist = self._get_json(r)
+            if profilelist:
+                if len(profilelist) > 1:
+                    LOG.error(_LE('Multiple replay profiles under name %s'),
+                              name)
+                    raise exception.VolumeBackendAPIException(
+                        _('Multiple profiles found.'))
+                return profilelist[0]
+        else:
+            LOG.error(_LE('find_replay_profile error %s'), r)
+        return None
+
+    def create_replay_profile(self, name):
+        '''Creates a replay profile on the Dell SC.
+
+        :param name: The ID of the consistency group.  This will be matched to
+                     the name on the Dell SC.
+        :return: SC profile or None.
+        '''
+        profile = self.find_replay_profile(name)
+        if not profile:
+            payload = {}
+            payload['StorageCenter'] = self.ssn
+            payload['Name'] = name
+            payload['Type'] = 'Consistent'
+            payload['Notes'] = self.notes
+            r = self.client.post('StorageCenter/ScReplayProfile',
+                                 payload)
+            if r.status_code == 201:
+                profile = self._first_result(r)
+            else:
+                LOG.error(_LE('create_replay_profile failed %s'), r)
+        return profile
+
+    def delete_replay_profile(self, profile):
+        '''Delete the replay profile from the Dell SC.
+
+        :param profile: SC replay profile.
+        :return: Nothing.
+        :raises: VolumeBackendAPIException
+        '''
+        r = self.client.delete('StorageCenter/ScReplayProfile/%s' %
+                               self._get_id(profile))
+        # 200 is a good return.  Log and leave.
+        if r.status_code == 200:
+            LOG.info(_LI('Profile %s has been deleted.'),
+                     profile.get('name'))
+        else:
+            # We failed due to a failure to delete an existing profile.
+            # This is reason to raise an exception.
+            LOG.error(_LE('Unable to delete profile %(cg)s : %(reason)s'),
+                      {'cg': profile.get('name'),
+                       'reason': r})
+            raise exception.VolumeBackendAPIException(
+                _('Error deleting replay profile.'))
+
+    def _get_volume_configuration(self, scvolume):
+        '''Get the ScVolumeConfiguration object.
+
+        :param scvolume: The Dell SC volume object.
+        :return: The SCVolumeConfiguration object or None.
+        '''
+        r = self.client.get('StorageCenter/ScVolume/%s/VolumeConfiguration' %
+                            self._get_id(scvolume))
+        if r.status_code == 200:
+            LOG.debug('get_volume_configuration %s', r)
+            return self._first_result(r)
+        return None
+
+    def _update_volume_profiles(self, scvolume, addid=None, removeid=None):
+        '''Either Adds or removes the listed profile from the SC volume.
+
+        :param scvolume: Dell SC volume object.
+        :param addid: Profile ID to be added to the SC volume configuration.
+        :param removeid: ID to be removed to the SC volume configuration.
+        :return: True/False on success/failure.
+        '''
+        if scvolume:
+            scvolumecfg = self._get_volume_configuration(scvolume)
+            if scvolumecfg:
+                profilelist = scvolumecfg.get('replayProfileList', [])
+                newprofilelist = []
+                # Do we have one to add?  Start the list with it.
+                if addid:
+                    newprofilelist = [addid]
+                # Re-add our existing profiles.
+                for profile in profilelist:
+                    profileid = self._get_id(profile)
+                    # Make sure it isn't one we want removed and that we
+                    # haven't already added it.  (IE it isn't the addid.)
+                    if (profileid != removeid and
+                       newprofilelist.count(profileid) == 0):
+                        newprofilelist.append(profileid)
+                # Update our volume configuration.
+                payload = {}
+                payload['ReplayProfileList'] = newprofilelist
+                r = self.client.put('StorageCenter/ScVolumeConfiguration/%s' %
+                                    self._get_id(scvolumecfg),
+                                    payload)
+                # check result
+                LOG.debug('_update_volume_profiles %s : %s : %s',
+                          self._get_id(scvolume),
+                          profilelist,
+                          r)
+                if r.status_code == 200:
+                    return True
+        return False
+
+    def _add_cg_volumes(self, profileid, add_volumes):
+        '''Trundles through add_volumes and adds the replay profile to them.
+
+        :param profileid: The ID of the replay profile.
+        :param add_volumes: List of Dell SC volume objects that are getting
+                            added to the consistency group.
+        :return: True/False on success/failure.
+        '''
+        for vol in add_volumes:
+            if (self._update_volume_profiles(self.find_volume(vol['id']),
+                                             addid=profileid,
+                                             removeid=None)):
+                LOG.info(_LI('Added %s to cg.'), vol['id'])
+            else:
+                LOG.error(_LE('Failed to add %s to cg.'), vol['id'])
+                return False
+        return True
+
+    def _remove_cg_volumes(self, profileid, remove_volumes):
+        '''Removes the replay profile from the remove_volumes list of vols.
+
+        :param profileid: The ID of the replay profile.
+        :param remove_volumes: List of Dell SC volume objects that are getting
+                               removed from the consistency group.
+        :return: True/False on success/failure.
+        '''
+        for vol in remove_volumes:
+            if (self._update_volume_profiles(self.find_volume(vol['id']),
+                                             addid=None,
+                                             removeid=profileid)):
+                LOG.info(_LI('Removed %s from cg.'), vol['id']),
+            else:
+                LOG.error(_LE('Failed to remove %s from cg.'), vol['id'])
+                return False
+        return True
+
+    def update_cg_volumes(self, profile, add_volumes=None,
+                          remove_volumes=None):
+        '''Adds or removes the profile from the specified volumes
+
+        :param profile: Dell SC replay profile object.
+        :param add_volumes: List of volumes we are adding to the consistency
+                            group. (Which is to say we are adding the profile
+                            to this list of volumes.)
+        :param remove_volumes: List of volumes we are removing from the
+                               consistency group. (Which is to say we are
+                               removing the profile from this list of volumes.)
+        :return: True/False on success/failure.
+        '''
+        ret = True
+        profileid = self._get_id(profile)
+        if add_volumes:
+            LOG.info(_LI('Adding volumes to cg %s.'), profile['name'])
+            ret = self._add_cg_volumes(profileid, add_volumes)
+        if ret and remove_volumes:
+            LOG.info(_LI('Removing volumes from cg %s.'), profile['name'])
+            ret = self._remove_cg_volumes(profileid, remove_volumes)
+        return ret
+
+    def snap_cg_replay(self, profile, replayid, expire):
+        '''Snaps a replay of a consistency group.
+
+        :param profile: The name of the consistency group profile.
+        :param replayid: The name of the replay.
+        :param expire: Time in mintues before a replay expires.  0 means no
+                       expiration.
+        :returns: Dell SC replay object.
+        '''
+        if profile:
+            payload = {}
+            payload['description'] = replayid
+            payload['expireTime'] = expire
+            r = self.client.post('StorageCenter/ScReplayProfile/%s/'
+                                 'CreateReplay'
+                                 % self._get_id(profile),
+                                 payload)
+            # 204 appears to be the correct return.
+            if r.status_code == 204:
+                LOG.debug('CreateReplay result %s', r)
+                return True
+
+            LOG.error(_LE('snap_cg error: %(code)d %(reason)s'),
+                      {'code': r.status_code,
+                       'reason': r.reason})
+        return False
+
+    def find_cg_replay(self, profile, replayid):
+        '''Searches for the replay by replayid.
+
+        replayid is stored in the replay's description attribute.
+
+        :param scvolume: Dell volume object.
+        :param replayid: Name to search for.  This is a portion of the
+                         snapshot ID as we do not have space for the entire
+                         GUID in the replay description.
+        :returns: Dell replay object or None.
+        '''
+        r = self.client.get('StorageCenter/ScReplayProfile/%s/ReplayList'
+                            % self._get_id(profile))
+        replays = self._get_json(r)
+        # This will be a list.  If it isn't bail
+        if isinstance(replays, list):
+            for replay in replays:
+                # The only place to save our information with the public
+                # api is the description field which isn't quite long
+                # enough.  So we check that our description is pretty much
+                # the max length and we compare that to the start of
+                # the snapshot id.
+                description = replay.get('description')
+                if (len(description) >= 30 and
+                        replayid.startswith(description) is True and
+                        replay.get('markedForExpiration') is not True):
+                    # We found our replay so return it.
+                    return replay
+        # If we are here then we didn't find the replay so warn and leave.
+        LOG.warning(_LW('Unable to find snapshot %s'),
+                    replayid)
+
+        return None
+
+    def delete_cg_replay(self, profile, replayid):
+        '''Finds a Dell replay by replayid string and expires it.
+
+        Once marked for expiration we do not return the replay as a snapshot
+        even though it might still exist.  (Backend requirements.)
+
+        :param cg_name: Consistency Group name.  This is the ReplayProfileName.
+        :param replayid: Name to search for.  This is a portion of the snapshot
+                         ID as we do not have space for the entire GUID in the
+                         replay description.
+        :returns: Boolean for success or failure.
+        '''
+
+        LOG.debug('Expiring consistency group replay %s', replayid)
+        replay = self.find_replay(profile,
+                                  replayid)
+        if replay is not None:
+            r = self.client.post('StorageCenter/ScReplay/%s/Expire'
+                                 % self._get_id(replay),
+                                 {})
+            if r.status_code != 204:
+                LOG.error(_LE('ScReplay Expire error: %(code)d %(reason)s'),
+                          {'code': r.status_code,
+                           'reason': r.reason})
+                return False
+        # We either couldn't find it or expired it.
+        return True
index adec896c5614ef3bc9b97a99add46cb7f2eb8492..f200cacf8fa4823b9b6f2249271ed540b040c9ae 100644 (file)
@@ -17,9 +17,10 @@ from oslo_log import log as logging
 from oslo_utils import excutils
 
 from cinder import exception
-from cinder.i18n import _, _LE, _LW
+from cinder.i18n import _, _LE, _LI, _LW
+from cinder.volume import driver
 from cinder.volume.drivers.dell import dell_storagecenter_api
-from cinder.volume.drivers.san import san
+from cinder.volume.drivers.san.san import san_opts
 from cinder.volume import volume_types
 
 
@@ -47,11 +48,12 @@ CONF = cfg.CONF
 CONF.register_opts(common_opts)
 
 
-class DellCommonDriver(san.SanDriver):
+class DellCommonDriver(driver.VolumeDriver):
 
     def __init__(self, *args, **kwargs):
         super(DellCommonDriver, self).__init__(*args, **kwargs)
         self.configuration.append_config_values(common_opts)
+        self.configuration.append_config_values(san_opts)
         self.backend_name =\
             self.configuration.safe_get('volume_backend_name') or 'Dell'
 
@@ -328,6 +330,7 @@ class DellCommonDriver(san.SanDriver):
             data['reserved_percentage'] = 0
             data['free_capacity_gb'] = 'unavailable'
             data['total_capacity_gb'] = 'unavailable'
+            data['consistencygroup_support'] = True
             # In theory if storageusage is None then we should have
             # blown up getting it.  If not just report unavailable.
             if storageusage is not None:
@@ -372,3 +375,158 @@ class DellCommonDriver(san.SanDriver):
         LOG.error(_LE('Unable to rename the logical volume for volume: %s'),
                   original_volume_name)
         return {'_name_id': new_volume['_name_id'] or new_volume['id']}
+
+    def create_consistencygroup(self, context, group):
+        '''This creates a replay profile on the storage backend.
+
+        :param context: the context of the caller.
+        :param group: the dictionary of the consistency group to be created.
+        :return: Nothing on success.
+        :raises: VolumeBackendAPIException
+        '''
+        gid = group['id']
+        with self._client.open_connection() as api:
+            cgroup = api.create_replay_profile(gid)
+            if cgroup:
+                LOG.info(_LI('Created Consistency Group %s'), gid)
+                return
+        raise exception.VolumeBackendAPIException(
+            _('Unable to create consistency group %s') % gid)
+
+    def delete_consistencygroup(self, context, group):
+        '''Delete the Dell SC profile associated with this consistency group.
+
+        :param context: the context of the caller.
+        :param group: the dictionary of the consistency group to be created.
+        :return: Updated model_update, volumes.
+        '''
+        gid = group['id']
+        with self._client.open_connection() as api:
+            profile = api.find_replay_profile(gid)
+            if profile:
+                api.delete_replay_profile(profile)
+        # If we are here because we found no profile that should be fine
+        # as we are trying to delete it anyway.
+
+        # Now whack the volumes.  So get our list.
+        volumes = self.db.volume_get_all_by_group(context, gid)
+        # Trundle through the list deleting the volumes.
+        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.
+
+        :param context: the context of the caller.
+        :param group: the dictionary of the consistency group to be updated.
+        :param add_volumes: a list of volume dictionaries to be added.
+        :param remove_volumes: a list of volume dictionaries to be removed.
+        :return model_update, add_volumes_update, remove_volumes_update
+
+        model_update is a dictionary that the driver wants the manager
+        to update upon a successful return. If None is returned, the manager
+        will set the status to 'available'.
+
+        add_volumes_update and remove_volumes_update are lists of dictionaries
+        that the driver wants the manager to update upon a successful return.
+        Note that each entry requires a {'id': xxx} so that the correct
+        volume entry can be updated. If None is returned, the volume will
+        remain its original status. Also note that you cannot directly
+        assign add_volumes to add_volumes_update as add_volumes is a list of
+        cinder.db.sqlalchemy.models.Volume objects and cannot be used for
+        db update directly. Same with remove_volumes.
+
+        If the driver throws an exception, the status of the group as well as
+        those of the volumes to be added/removed will be set to 'error'.
+        '''
+        gid = group['id']
+        with self._client.open_connection() as api:
+            profile = api.find_replay_profile(gid)
+            if not profile:
+                LOG.error(_LE('Cannot find Consistency Group %s'), gid)
+            elif api.update_cg_volumes(profile,
+                                       add_volumes,
+                                       remove_volumes):
+                LOG.info(_LI('Updated Consistency Group %s'), gid)
+                # we need nothing updated above us so just return None.
+                return None, None, None
+        # Things did not go well so throw.
+        raise exception.VolumeBackendAPIException(
+            _('Unable to update consistency group %s') % gid)
+
+    def create_cgsnapshot(self, context, cgsnapshot):
+        '''Takes a snapshot of the consistency group.
+
+        :param context: the context of the caller.
+        :param cgsnapshot: Information about the snapshot to take.
+        :return: Updated model_update, snapshots.
+        :raises: VolumeBackendAPIException.
+        '''
+        cgid = cgsnapshot['consistencygroup_id']
+        snapshotid = cgsnapshot['id']
+
+        with self._client.open_connection() as api:
+            profile = api.find_replay_profile(cgid)
+            if profile:
+                LOG.debug('profile %s replayid %s', profile, snapshotid)
+                if api.snap_cg_replay(profile, snapshotid, 0):
+                    snapshots = self.db.snapshot_get_all_for_cgsnapshot(
+                        context,
+                        snapshotid)
+                    LOG.debug(snapshots)
+                    for snapshot in snapshots:
+                        LOG.debug(snapshot)
+                        snapshot['status'] = 'available'
+
+                    model_update = {'status': 'available'}
+
+                    return model_update, snapshots
+
+                # That didn't go well.  Tell them why.  Then bomb out.
+                LOG.error(_LE('Failed to snap Consistency Group %s'), cgid)
+            else:
+                LOG.error(_LE('Cannot find Consistency Group %s'), cgid)
+
+        raise exception.VolumeBackendAPIException(
+            _('Unable to snap Consistency Group %s') % cgid)
+
+    def delete_cgsnapshot(self, context, cgsnapshot):
+        '''Deletes a cgsnapshot.
+
+        If profile isn't found return success.  If failed to delete the
+        replay (the snapshot) then raise an exception.
+
+        :param context: the context of the caller.
+        :param cgsnapshot: Information about the snapshot to delete.
+        :return: Updated model_update, snapshots.
+        :raises: VolumeBackendAPIException.
+        '''
+        cgid = cgsnapshot['consistencygroup_id']
+        snapshotid = cgsnapshot['id']
+
+        with self._client.open_connection() as api:
+            profile = api.find_replay_profile(cgid)
+            if profile:
+                LOG.info(_LI('Deleting snapshot %(ss)s from %(pro)s'),
+                         {'ss': snapshotid,
+                          'pro': profile})
+                if not api.delete_cg_replay(profile, snapshotid):
+                    raise exception.VolumeBackendAPIException(
+                        _('Unable to delete Consistency Group snapshot %s') %
+                        snapshotid)
+
+            snapshots = self.db.snapshot_get_all_for_cgsnapshot(context,
+                                                                snapshotid)
+
+            for snapshot in snapshots:
+                snapshot['status'] = 'deleted'
+
+            model_update = {'status': 'deleted'}
+
+            return model_update, snapshots
index 32b0465b7b36121a7583ca39c3e106ffa82f08f7..fa727b1f1f7d5fe2694b6c7dddd75e297643b4d6 100644 (file)
@@ -37,9 +37,10 @@ class DellStorageCenterFCDriver(dell_storagecenter_common.DellCommonDriver,
     Version history:
         1.0.0 - Initial driver
         1.1.0 - Added extra spec support for Storage Profile selection
+        1.2.0 - Added consistency group support.
     '''
 
-    VERSION = '1.1.0'
+    VERSION = '1.2.0'
 
     def __init__(self, *args, **kwargs):
         super(DellStorageCenterFCDriver, self).__init__(*args, **kwargs)
index 92faa037885ef032fbda1ae181b8c22b4d68c6d6..4ee93974e1d42bbfdc10e89667fc26bcc84aa54a 100644 (file)
@@ -19,14 +19,13 @@ from oslo_utils import excutils
 
 from cinder import exception
 from cinder.i18n import _, _LE, _LI
+from cinder.volume import driver
 from cinder.volume.drivers.dell import dell_storagecenter_common
-from cinder.volume.drivers import san
-
 LOG = logging.getLogger(__name__)
 
 
-class DellStorageCenterISCSIDriver(san.SanISCSIDriver,
-                                   dell_storagecenter_common.DellCommonDriver):
+class DellStorageCenterISCSIDriver(dell_storagecenter_common.DellCommonDriver,
+                                   driver.ISCSIDriver):
 
     '''Implements commands for Dell StorageCenter ISCSI management.
 
@@ -36,9 +35,10 @@ class DellStorageCenterISCSIDriver(san.SanISCSIDriver,
     Version history:
         1.0.0 - Initial driver
         1.1.0 - Added extra spec support for Storage Profile selection
+        1.2.0 - Added consistency group support.
     '''
 
-    VERSION = '1.1.0'
+    VERSION = '1.2.0'
 
     def __init__(self, *args, **kwargs):
         super(DellStorageCenterISCSIDriver, self).__init__(*args, **kwargs)