From a300ebf0db8b02c13bdf0ad65bc38f57d2bc976d Mon Sep 17 00:00:00 2001 From: Tom Swanson Date: Wed, 8 Jul 2015 10:46:09 -0500 Subject: [PATCH] Dell SC: Add support for ManageableVD Added support for the ManageableVD volume driver class. This also meant the driver changed from inheriting the volumedriver class to inheriting the assorted *VD classes. A few things about our ManageableVD support. We only allow management of a volume if it is sized in multiples of 1GB. We only allow management of volumes that are not currently mapped to a server on the Storage Center. id-types of source-id (DeviceId on the Storage Center) and source-name (Volume Name on the Storage Center) are supported. Change-Id: Iecc93ce1654111e1292cfc1931d1eda4abd08595 --- cinder/tests/unit/test_dellsc.py | 116 ++++++ cinder/tests/unit/test_dellscapi.py | 392 +++++++++++++++++- .../drivers/dell/dell_storagecenter_api.py | 190 +++++++-- .../drivers/dell/dell_storagecenter_common.py | 79 +++- .../drivers/dell/dell_storagecenter_fc.py | 5 +- .../drivers/dell/dell_storagecenter_iscsi.py | 5 +- 6 files changed, 751 insertions(+), 36 deletions(-) diff --git a/cinder/tests/unit/test_dellsc.py b/cinder/tests/unit/test_dellsc.py index a99fdd60b..c3f1cc1e5 100644 --- a/cinder/tests/unit/test_dellsc.py +++ b/cinder/tests/unit/test_dellsc.py @@ -1451,3 +1451,119 @@ class DellSCSanISCSIDriverTestCase(test.TestCase): cgsnap['consistencygroup_id']) mock_delete_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE, cgsnap['id']) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'manage_existing') + def test_manage_existing(self, + mock_manage_existing, + mock_close_connection, + mock_open_connection, + mock_init): + # Very little to do in this one. The call is sent + # straight down. + volume = {'id': 'guid'} + existing_ref = {'source-name': 'imavolumename'} + self.driver.manage_existing(volume, existing_ref) + mock_manage_existing.assert_called_once_with(volume['id'], + existing_ref) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'manage_existing') + def test_manage_existing_id(self, + mock_manage_existing, + mock_close_connection, + mock_open_connection, + mock_init): + # Very little to do in this one. The call is sent + # straight down. + volume = {'id': 'guid'} + existing_ref = {'source-id': 'imadeviceid'} + self.driver.manage_existing(volume, existing_ref) + mock_manage_existing.assert_called_once_with(volume['id'], + existing_ref) + + def test_manage_existing_bad_ref(self, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'guid'} + existing_ref = {'banana-name': 'imavolumename'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, + volume, + existing_ref) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_unmanaged_volume_size', + return_value=4) + def test_manage_existing_get_size(self, + mock_get_unmanaged_volume_size, + mock_close_connection, + mock_open_connection, + mock_init): + # Almost nothing to test here. Just that we call our function. + volume = {'id': 'guid'} + existing_ref = {'source-name': 'imavolumename'} + res = self.driver.manage_existing_get_size(volume, existing_ref) + mock_get_unmanaged_volume_size.assert_called_once_with(existing_ref) + # The above is 4GB and change. + self.assertEqual(4, res) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_unmanaged_volume_size', + return_value=4) + def test_manage_existing_get_size_id(self, + mock_get_unmanaged_volume_size, + mock_close_connection, + mock_open_connection, + mock_init): + # Almost nothing to test here. Just that we call our function. + volume = {'id': 'guid'} + existing_ref = {'source-id': 'imadeviceid'} + res = self.driver.manage_existing_get_size(volume, existing_ref) + mock_get_unmanaged_volume_size.assert_called_once_with(existing_ref) + # The above is 4GB and change. + self.assertEqual(4, res) + + def test_manage_existing_get_size_bad_ref(self, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'guid'} + existing_ref = {'banana-name': 'imavolumename'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, + volume, + existing_ref) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmanage') + def test_unmanage(self, + mock_unmanage, + mock_find_volume, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'guid'} + self.driver.unmanage(volume) + mock_find_volume.assert_called_once_with(volume['id']) + mock_unmanage.assert_called_once_with(self.VOLUME) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmanage') + def test_unmanage_volume_not_found(self, + mock_unmanage, + mock_find_volume, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'guid'} + self.driver.unmanage(volume) + mock_find_volume.assert_called_once_with(volume['id']) + self.assertFalse(mock_unmanage.called) diff --git a/cinder/tests/unit/test_dellscapi.py b/cinder/tests/unit/test_dellscapi.py index 6fc071a45..345d9ab5d 100644 --- a/cinder/tests/unit/test_dellscapi.py +++ b/cinder/tests/unit/test_dellscapi.py @@ -2105,7 +2105,7 @@ class DellSCSanAPITestCase(test.TestCase): mock_open_connection, mock_init): # Test case to find volume in the configured volume folder - res = self.scapi._get_volume_list(self.volume_name, True) + res = self.scapi._get_volume_list(self.volume_name, None, True) self.assertTrue(mock_post.called) self.assertTrue(mock_get_json.called) self.assertEqual(self.VOLUME_LIST, res, 'Unexpected volume list') @@ -2123,17 +2123,17 @@ class DellSCSanAPITestCase(test.TestCase): mock_open_connection, mock_init): # Test case to find volume anywhere in the configured SC - res = self.scapi._get_volume_list(self.volume_name, False) + res = self.scapi._get_volume_list(self.volume_name, None, False) self.assertTrue(mock_post.called) self.assertTrue(mock_get_json.called) self.assertEqual(self.VOLUME_LIST, res, 'Unexpected volume list') - def test__get_volume_list_no_name(self, - mock_close_connection, - mock_open_connection, - mock_init): - # Test case specified volume name is None - res = self.scapi._get_volume_list(None, True) + def test_get_volume_list_no_name_no_id(self, + mock_close_connection, + mock_open_connection, + mock_init): + # Test case specified volume name is None and device id is None. + res = self.scapi._get_volume_list(None, None, True) self.assertIsNone(res, 'None expected') @mock.patch.object(dell_storagecenter_api.HttpClient, @@ -2145,7 +2145,7 @@ class DellSCSanAPITestCase(test.TestCase): mock_open_connection, mock_init): # Test case to find volume in the configured volume folder - res = self.scapi._get_volume_list(self.volume_name, True) + res = self.scapi._get_volume_list(self.volume_name, None, True) self.assertTrue(mock_post.called) self.assertIsNone(res, 'None expected') @@ -4689,6 +4689,380 @@ class DellSCSanAPITestCase(test.TestCase): self.assertTrue(mock_find_replay.called) self.assertTrue(res) + def test_size_to_gb(self, + mock_close_connection, + mock_open_connection, + mock_init): + gb, rem = self.scapi._size_to_gb('1.073741824E9 Byte') + self.assertEqual(1, gb) + self.assertEqual(0, rem) + self.assertRaises(exception.VolumeBackendAPIException, + self.scapi._size_to_gb, + 'banana') + gb, rem = self.scapi._size_to_gb('1.073741924E9 Byte') + self.assertEqual(1, gb) + self.assertEqual(100, rem) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741824E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 0)) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_mappings', + return_value=[]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_volume_folder', + return_value={'id': '1'}) + @mock.patch.object(dell_storagecenter_api.HttpClient, + 'put', + return_value=RESPONSE_200) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_id') + def test_manage_existing(self, + mock_get_id, + mock_put, + mock_find_volume_folder, + mock_find_mappings, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + newname = 'guid' + existing = {'source-name': 'scvolname'} + # First call is foldername, second is vollist. This is reflected + # in the payload. + mock_get_id.side_effect = ['1', '100'] + expected_url = 'StorageCenter/ScVolume/100' + expected_payload = {'Name': newname, + 'VolumeFolder': '1'} + self.scapi.manage_existing(newname, existing) + mock_get_volume_list.asert_called_once_with(existing, False) + self.assertTrue(mock_get_id.called) + mock_put.assert_called_once_with(expected_url, expected_payload) + self.assertTrue(mock_find_volume_folder.called) + self.assertTrue(mock_find_mappings.called) + self.assertTrue(mock_size_to_gb.called) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741824E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 0)) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_mappings', + return_value=[]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_volume_folder', + return_value=None) + @mock.patch.object(dell_storagecenter_api.HttpClient, + 'put', + return_value=RESPONSE_200) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_id', + return_value='100') + def test_manage_existing_folder_not_found(self, + mock_get_id, + mock_put, + mock_find_volume_folder, + mock_find_mappings, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + # Same as above only we don't have a volume folder. + newname = 'guid' + existing = {'source-name': 'scvolname'} + expected_url = 'StorageCenter/ScVolume/100' + expected_payload = {'Name': newname} + self.scapi.manage_existing(newname, existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + mock_put.assert_called_once_with(expected_url, expected_payload) + self.assertTrue(mock_get_id.called) + self.assertTrue(mock_find_volume_folder.called) + self.assertTrue(mock_find_mappings.called) + self.assertTrue(mock_size_to_gb.called) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[]) + def test_manage_existing_vol_not_found(self, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + + # Same as above only we don't have a volume folder. + newname = 'guid' + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.scapi.manage_existing, + newname, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{}, {}, {}]) + def test_manage_existing_vol_multiple_found(self, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + + # Same as above only we don't have a volume folder. + newname = 'guid' + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.scapi.manage_existing, + newname, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741924E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 100)) + def test_manage_existing_bad_size(self, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + + # Same as above only we don't have a volume folder. + newname = 'guid' + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.VolumeBackendAPIException, + self.scapi.manage_existing, + newname, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + self.assertTrue(mock_size_to_gb.called) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741824E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 0)) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_mappings', + return_value=[{}, {}]) + def test_manage_existing_already_mapped(self, + mock_find_mappings, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + + newname = 'guid' + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.VolumeBackendAPIException, + self.scapi.manage_existing, + newname, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + self.assertTrue(mock_find_mappings.called) + self.assertTrue(mock_size_to_gb.called) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741824E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 0)) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_mappings', + return_value=[]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_find_volume_folder', + return_value=None) + @mock.patch.object(dell_storagecenter_api.HttpClient, + 'put', + return_value=RESPONSE_400) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_id', + return_value='100') + def test_manage_existing_rename_fail(self, + mock_get_id, + mock_put, + mock_find_volume_folder, + mock_find_mappings, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + # We fail on the _find_volume_folder to make this easier. + newname = 'guid' + existing = {'source-name': 'scvolname'} + expected_url = 'StorageCenter/ScVolume/100' + expected_payload = {'Name': newname} + self.assertRaises(exception.VolumeBackendAPIException, + self.scapi.manage_existing, + newname, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + self.assertTrue(mock_get_id.called) + mock_put.assert_called_once_with(expected_url, expected_payload) + self.assertTrue(mock_find_volume_folder.called) + self.assertTrue(mock_find_mappings.called) + self.assertTrue(mock_size_to_gb.called) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741824E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 0)) + def test_get_unmanaged_volume_size(self, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + existing = {'source-name': 'scvolname'} + res = self.scapi.get_unmanaged_volume_size(existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + self.assertTrue(mock_size_to_gb.called) + self.assertEqual(1, res) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[]) + def test_get_unmanaged_volume_size_not_found(self, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.scapi.get_unmanaged_volume_size, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{}, {}, {}]) + def test_get_unmanaged_volume_size_many_found(self, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.scapi.get_unmanaged_volume_size, + existing) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_volume_list', + return_value=[{'configuredSize': + '1.073741924E9 Bytes'}]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_size_to_gb', + return_value=(1, 100)) + def test_get_unmanaged_volume_size_bad_size(self, + mock_size_to_gb, + mock_get_volume_list, + mock_close_connection, + mock_open_connection, + mock_init): + existing = {'source-name': 'scvolname'} + self.assertRaises(exception.VolumeBackendAPIException, + self.scapi.get_unmanaged_volume_size, + existing) + self.assertTrue(mock_size_to_gb.called) + mock_get_volume_list.asert_called_once_with( + existing.get('source-name'), + existing.get('source-id'), + False) + + @mock.patch.object(dell_storagecenter_api.HttpClient, + 'put', + return_value=RESPONSE_200) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_id', + return_value='100') + def test_unmanage(self, + mock_get_id, + mock_put, + mock_close_connection, + mock_open_connection, + mock_init): + # Same as above only we don't have a volume folder. + scvolume = {'name': 'guid'} + expected_url = 'StorageCenter/ScVolume/100' + newname = 'Unmanaged_' + scvolume['name'] + expected_payload = {'Name': newname} + self.scapi.unmanage(scvolume) + self.assertTrue(mock_get_id.called) + mock_put.assert_called_once_with(expected_url, expected_payload) + + @mock.patch.object(dell_storagecenter_api.HttpClient, + 'put', + return_value=RESPONSE_400) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '_get_id', + return_value='100') + def test_unmanage_fail(self, + mock_get_id, + mock_put, + mock_close_connection, + mock_open_connection, + mock_init): + # Same as above only we don't have a volume folder. + scvolume = {'name': 'guid'} + expected_url = 'StorageCenter/ScVolume/100' + newname = 'Unmanaged_' + scvolume['name'] + expected_payload = {'Name': newname} + self.assertRaises(exception.VolumeBackendAPIException, + self.scapi.unmanage, + scvolume) + self.assertTrue(mock_get_id.called) + mock_put.assert_called_once_with(expected_url, expected_payload) + 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 ddc38542b..95352f303 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.2.0' + APIVERSION = '2.0.1' def __init__(self, host, port, user, password, verify): '''This creates a connection to Dell Enterprise Manager. @@ -605,36 +605,39 @@ class StorageCenterApi(object): return scvolume - def _get_volume_list(self, name, filterbyvfname=True): + def _get_volume_list(self, name, deviceid, filterbyvfname=True): '''Return the specified list of volumes. :param name: Volume name. + :param deviceid: Volume device ID on the SC backend. :param filterbyvfname: If set to true then this filters by the preset folder name. - :return: Returns the scvolume or None. + :return: Returns the scvolume list or None. ''' result = None - pf = PayloadFilter() - pf.append('scSerialNumber', self.ssn) - # We need a name to find a volume. - if name is not None: - pf.append('Name', name) - else: - return None - # set folderPath - if filterbyvfname: - vfname = (self.vfname if self.vfname.endswith('/') - else self.vfname + '/') - pf.append('volumeFolderPath', vfname) - r = self.client.post('StorageCenter/ScVolume/GetList', - pf.payload) - if r.status_code != 200: - LOG.debug('ScVolume GetList error %(name)s: %(code)d %(reason)s', - {'name': name, - 'code': r.status_code, - 'reason': r.reason}) - else: - result = self._get_json(r) + # We need a name or a device ID to find a volume. + if name or deviceid: + pf = PayloadFilter() + pf.append('scSerialNumber', self.ssn) + if name is not None: + pf.append('Name', name) + if deviceid is not None: + pf.append('DeviceId', deviceid) + # set folderPath + if filterbyvfname: + vfname = (self.vfname if self.vfname.endswith('/') + else self.vfname + '/') + pf.append('volumeFolderPath', vfname) + r = self.client.post('StorageCenter/ScVolume/GetList', + pf.payload) + if r.status_code != 200: + LOG.debug('ScVolume GetList error ' + '%(name)s: %(code)d %(reason)s', + {'name': name, + 'code': r.status_code, + 'reason': r.reason}) + else: + result = self._get_json(r) # We return None if there was an error and a list if the command # succeeded. It might be an empty list. return result @@ -661,6 +664,7 @@ class StorageCenterApi(object): # Look for our volume in our folder. vollist = self._get_volume_list(name, + None, True) # If an empty list was returned they probably moved the volumes or # changed the folder name so try again without the folder. @@ -669,6 +673,7 @@ class StorageCenterApi(object): {'n': name, 'v': self.vfname}) vollist = self._get_volume_list(name, + None, False) # If multiple volumes of the same name are found we need to error. @@ -1825,3 +1830,140 @@ class StorageCenterApi(object): return False # We either couldn't find it or expired it. return True + + def _size_to_gb(self, spacestring): + '''Splits a SC size string into GB and a remainder. + + Space is returned in a string like ... + 7.38197504E8 Bytes + Need to split that apart and convert to GB. + + :param spacestring: SC size string. + :return: Size in GB and remainder in byte. + ''' + try: + n = spacestring.split(' ', 1) + fgb = int(float(n[0]) // 1073741824) + frem = int(float(n[0]) % 1073741824) + return fgb, frem + + except Exception: + # We received an invalid size string. Blow up. + raise exception.VolumeBackendAPIException( + _('Error retrieving volume size')) + + def manage_existing(self, newname, existing): + '''Finds the volume named existing and renames it. + + This checks a few things. The volume has to exist. There can + only be one volume by that name. Since cinder manages volumes + by the GB it has to be defined on a GB boundry. + + This renames existing to newname. newname is the guid from + the cinder volume['id']. The volume is moved to the defined + cinder volume folder. + + :param newname: Name to rename the volume to. + :param existing: The existing volume dict.. + :return: Nothing. + :raises: VolumeBackendAPIException, ManageExistingInvalidReference + ''' + vollist = self._get_volume_list(existing.get('source-name'), + existing.get('source-id'), + False) + count = len(vollist) + # If we found one volume with that name we can work with it. + if count == 1: + # First thing to check is if the size is something we can + # work with. + sz, rem = self._size_to_gb(vollist[0]['configuredSize']) + if rem > 0: + raise exception.VolumeBackendAPIException( + _('Volume size must multiple of 1 GB.')) + + # We only want to grab detached volumes. + mappings = self._find_mappings(vollist[0]) + if len(mappings) > 0: + raise exception.VolumeBackendAPIException( + _('Volume is attached to a server. (%s)') % existing) + + # Find our folder + folder = self._find_volume_folder(True) + + # If we actually have a place to put our volume create it + if folder is None: + LOG.warning(_LW('Unable to create folder %s'), + self.vfname) + + # Rename and move our volume. + payload = {} + payload['Name'] = newname + if folder: + payload['VolumeFolder'] = self._get_id(folder) + + r = self.client.put('StorageCenter/ScVolume/%s' % + self._get_id(vollist[0]), + payload) + if r.status_code != 200: + LOG.error(_LE('ScVolume error on rename: %(code)d %(reason)s'), + {'code': r.status_code, + 'reason': r.reason}) + raise exception.VolumeBackendAPIException( + _('Unable to manage volume %s') % existing) + elif count > 1: + raise exception.ManageExistingInvalidReference( + _('Volume not unique. (%s)') % existing) + else: + raise exception.ManageExistingInvalidReference( + _('Volume not found. (%s)') % existing) + + def get_unmanaged_volume_size(self, existing): + '''Looks up the volume named existing and returns its size string. + + :param existing: Existing volume dict. + :return: The SC configuredSize string. + :raises: ManageExistingInvalidReference + ''' + vollist = self._get_volume_list(existing.get('source-name'), + existing.get('source-id'), + False) + count = len(vollist) + # If we found one volume with that name we can work with it. + if count == 1: + sz, rem = self._size_to_gb(vollist[0]['configuredSize']) + if rem > 0: + raise exception.VolumeBackendAPIException( + _('Volume size must multiple of 1 GB.')) + return sz + elif count > 1: + raise exception.ManageExistingInvalidReference( + _('Volume not unique. (%s)') % existing) + else: + raise exception.ManageExistingInvalidReference( + _('Volume not found. (%s)') % existing) + + def unmanage(self, scvolume): + '''Unmanage our volume. + + We simply rename with with a prefix of 'Unmanaged_'. That's it. + + :param scvolume: The Dell SC volume object. + :return: Nothing. + :raises: VolumeBackendAPIException + ''' + newname = 'Unmanaged_' + scvolume['name'] + payload = {} + payload['Name'] = newname + r = self.client.put('StorageCenter/ScVolume/%s' % + self._get_id(scvolume), + payload) + if r.status_code == 200: + LOG.info(_LI('Volume %s unmanaged.'), scvolume['name']) + else: + LOG.error(_LE('ScVolume error on rename: %(code)d %(reason)s'), + {'code': r.status_code, + 'reason': r.reason}) + raise exception.VolumeBackendAPIException( + _('Unable to rename volume %(existing)s to %(newname)s') % + {'existing': scvolume['name'], + 'newname': newname}) diff --git a/cinder/volume/drivers/dell/dell_storagecenter_common.py b/cinder/volume/drivers/dell/dell_storagecenter_common.py index f200cacf8..2c8ea40cf 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_common.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_common.py @@ -48,7 +48,9 @@ CONF = cfg.CONF CONF.register_opts(common_opts) -class DellCommonDriver(driver.VolumeDriver): +class DellCommonDriver(driver.ConsistencyGroupVD, driver.ManageableVD, + driver.ExtendVD, driver.CloneableVD, driver.SnapshotVD, + driver.BaseVD): def __init__(self, *args, **kwargs): super(DellCommonDriver, self).__init__(*args, **kwargs) @@ -374,6 +376,7 @@ class DellCommonDriver(driver.VolumeDriver): # The world was horrible to us so we should error and leave. 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): @@ -530,3 +533,77 @@ class DellCommonDriver(driver.VolumeDriver): model_update = {'status': 'deleted'} return model_update, snapshots + + def manage_existing(self, volume, existing_ref): + """Brings an existing backend storage object under Cinder management. + + existing_ref is passed straight through from the API request's + manage_existing_ref value, and it is up to the driver how this should + be interpreted. It should be sufficient to identify a storage object + that the driver should somehow associate with the newly-created cinder + volume structure. + + There are two ways to do this: + + 1. Rename the backend storage object so that it matches the, + volume['name'] which is how drivers traditionally map between a + cinder volume and the associated backend storage object. + + 2. Place some metadata on the volume, or somewhere in the backend, that + allows other driver requests (e.g. delete, clone, attach, detach...) + to locate the backend storage object when required. + + If the existing_ref doesn't make sense, or doesn't refer to an existing + backend storage object, raise a ManageExistingInvalidReference + exception. + + The volume may have a volume_type, and the driver can inspect that and + compare against the properties of the referenced backend storage + object. If they are incompatible, raise a + ManageExistingVolumeTypeMismatch, specifying a reason for the failure. + + :param volume: Cinder volume to manage + :param existing_ref: Driver-specific information used to identify a + volume + """ + if existing_ref.get('source-name') or existing_ref.get('source-id'): + with self._client.open_connection() as api: + api.manage_existing(volume['id'], existing_ref) + else: + raise exception.ManageExistingInvalidReference( + _('Must specify source-name or source-id. (%s)') % + existing_ref) + + def manage_existing_get_size(self, volume, existing_ref): + """Return size of volume to be managed by manage_existing. + + When calculating the size, round up to the next GB. + + :param volume: Cinder volume to manage + :param existing_ref: Driver-specific information used to identify a + volume + """ + if existing_ref.get('source-name') or existing_ref.get('source-id'): + with self._client.open_connection() as api: + return api.get_unmanaged_volume_size(existing_ref) + else: + raise exception.ManageExistingInvalidReference( + _('Must specify source-name or source-id. (%s)') % + existing_ref) + + def unmanage(self, volume): + """Removes the specified volume from Cinder management. + + Does not delete the underlying backend storage object. + + For most drivers, this will not need to do anything. However, some + drivers might use this call as an opportunity to clean up any + Cinder-specific configuration that they have associated with the + backend storage object. + + :param volume: Cinder volume to unmanage + """ + with self._client.open_connection() as api: + scvolume = api.find_volume(volume['id']) + if scvolume: + api.unmanage(scvolume) diff --git a/cinder/volume/drivers/dell/dell_storagecenter_fc.py b/cinder/volume/drivers/dell/dell_storagecenter_fc.py index fa727b1f1..6944d5bd7 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_fc.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_fc.py @@ -38,9 +38,12 @@ class DellStorageCenterFCDriver(dell_storagecenter_common.DellCommonDriver, 1.0.0 - Initial driver 1.1.0 - Added extra spec support for Storage Profile selection 1.2.0 - Added consistency group support. + 2.0.0 - Switched to inheriting functional objects rather than volume + driver. + 2.1.0 - Added support for ManageableVD. ''' - VERSION = '1.2.0' + VERSION = '2.1.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 4ee93974e..f6b290ca9 100644 --- a/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py +++ b/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py @@ -36,9 +36,12 @@ class DellStorageCenterISCSIDriver(dell_storagecenter_common.DellCommonDriver, 1.0.0 - Initial driver 1.1.0 - Added extra spec support for Storage Profile selection 1.2.0 - Added consistency group support. + 2.0.0 - Switched to inheriting functional objects rather than volume + driver. + 2.1.0 - Added support for ManageableVD. ''' - VERSION = '1.2.0' + VERSION = '2.1.0' def __init__(self, *args, **kwargs): super(DellStorageCenterISCSIDriver, self).__init__(*args, **kwargs) -- 2.45.2