From 55321e371ff21f985800bbbaa36bc544369beeb9 Mon Sep 17 00:00:00 2001 From: tswanson Date: Mon, 29 Jun 2015 14:21:45 -0500 Subject: [PATCH] Dell SC: Add support for consistency groups 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 | 334 +++++++++ cinder/tests/unit/test_dellscapi.py | 697 +++++++++++++++++- .../drivers/dell/dell_storagecenter_api.py | 274 ++++++- .../drivers/dell/dell_storagecenter_common.py | 164 ++++- .../drivers/dell/dell_storagecenter_fc.py | 3 +- .../drivers/dell/dell_storagecenter_iscsi.py | 10 +- 6 files changed, 1471 insertions(+), 11 deletions(-) diff --git a/cinder/tests/unit/test_dellsc.py b/cinder/tests/unit/test_dellsc.py index 95bc8c674..a99fdd60b 100644 --- a/cinder/tests/unit/test_dellsc.py +++ b/cinder/tests/unit/test_dellsc.py @@ -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']) diff --git a/cinder/tests/unit/test_dellscapi.py b/cinder/tests/unit/test_dellscapi.py index aaa1ee2f3..6fc071a45 100644 --- a/cinder/tests/unit/test_dellscapi.py +++ b/cinder/tests/unit/test_dellscapi.py @@ -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): diff --git a/cinder/volume/drivers/dell/dell_storagecenter_api.py b/cinder/volume/drivers/dell/dell_storagecenter_api.py index fa85dbb3b..ddc38542b 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_api.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_api.py @@ -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 diff --git a/cinder/volume/drivers/dell/dell_storagecenter_common.py b/cinder/volume/drivers/dell/dell_storagecenter_common.py index adec896c5..f200cacf8 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_common.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_common.py @@ -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 diff --git a/cinder/volume/drivers/dell/dell_storagecenter_fc.py b/cinder/volume/drivers/dell/dell_storagecenter_fc.py index 32b0465b7..fa727b1f1 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_fc.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_fc.py @@ -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) diff --git a/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py b/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py index 92faa0378..4ee93974e 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py @@ -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) -- 2.45.2