From 8d9f6be9de26fd10db46384245f36da82beefd02 Mon Sep 17 00:00:00 2001 From: chenzongliang Date: Sat, 12 Dec 2015 17:11:55 +0800 Subject: [PATCH] Huawei: Add manage/unmanage snapshot support Add manage/unmanage snapshot support for Huawei drivers. Also implement the required manage_existing_snapshot_get_size function. DocImpact Implements: blueprint huawei-manage-unmanage-snapshot Change-Id: I05f8a750a745498c879d8c734e661d778528258c --- cinder/tests/unit/test_huawei_drivers.py | 157 +++++++++++++++++- cinder/volume/drivers/huawei/constants.py | 1 + cinder/volume/drivers/huawei/huawei_driver.py | 103 +++++++++++- cinder/volume/drivers/huawei/rest_client.py | 26 ++- ...ge-unmanage-snapshot-e35ff844d72fedfb.yaml | 2 + 5 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/huawei-manage-unmanage-snapshot-e35ff844d72fedfb.yaml diff --git a/cinder/tests/unit/test_huawei_drivers.py b/cinder/tests/unit/test_huawei_drivers.py index 8e75023d4..2f748565a 100644 --- a/cinder/tests/unit/test_huawei_drivers.py +++ b/cinder/tests/unit/test_huawei_drivers.py @@ -1640,7 +1640,7 @@ class HuaweiISCSIDriverTestCase(test.TestCase): def test_get_volume_status(self): data = self.driver.get_volume_stats() - self.assertEqual('2.0.2', data['driver_version']) + self.assertEqual('2.0.3', data['driver_version']) def test_extend_volume(self): lun_info = self.driver.extend_volume(test_volume, 3) @@ -2162,6 +2162,159 @@ class HuaweiISCSIDriverTestCase(test.TestCase): self.driver.unmanage(test_volume) self.assertEqual(ddt_data[1], mock_rename.call_count) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_info', + return_value={'ID': 'ID1', + 'NAME': 'test1', + 'PARENTID': '12', + 'USERCAPACITY': 2097152, + 'HEALTHSTATUS': '2'}) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_id_by_name', + return_value='ID1') + def test_manage_existing_snapshot_abnormal(self, mock_get_by_name, + mock_get_info): + with mock.patch.object(huawei_driver.HuaweiBaseDriver, + '_get_snapshot_info_by_ref', + return_value={'HEALTHSTATUS': '2', + 'PARENTID': '12'}): + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + external_ref = {'source-name': 'test1'} + ex = self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + test_snapshot, external_ref) + self.assertIsNotNone(re.search('Snapshot status is not normal', + ex.msg)) + + @mock.patch.object(rest_client.RestClient, 'get_snapshot_info', + return_value={'ID': 'ID1', + 'EXPOSEDTOINITIATOR': 'true', + 'NAME': 'test1', + 'PARENTID': '12', + 'USERCAPACITY': 2097152, + 'HEALTHSTATUS': constants.STATUS_HEALTH}) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_id_by_name', + return_value='ID1') + def test_manage_existing_snapshot_with_lungroup(self, mock_get_by_name, + mock_get_info): + # Already in LUN group. + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + external_ref = {'source-name': 'test1'} + ex = self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + test_snapshot, external_ref) + self.assertIsNotNone(re.search('Snapshot is exposed to initiator', + ex.msg)) + + @mock.patch.object(rest_client.RestClient, 'rename_snapshot') + @mock.patch.object(huawei_driver.HuaweiBaseDriver, + '_get_snapshot_info_by_ref', + return_value={'ID': 'ID1', + 'EXPOSEDTOINITIATOR': 'false', + 'NAME': 'test1', + 'PARENTID': '12', + 'USERCAPACITY': 2097152, + 'HEALTHSTATUS': constants.STATUS_HEALTH}) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_info', + return_value={'ID': 'ID1', + 'EXPOSEDTOINITIATOR': 'false', + 'NAME': 'test1', + 'PARENTID': '12', + 'USERCAPACITY': 2097152, + 'HEALTHSTATUS': constants.STATUS_HEALTH}) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_id_by_name', + return_value='ID1') + def test_manage_existing_snapshot_success(self, mock_get_by_name, + mock_get_info, + mock_check_snapshot, + mock_rename): + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + external_ref = {'source-name': 'test1'} + model_update = self.driver.manage_existing_snapshot(test_snapshot, + external_ref) + self.assertEqual({'provider_location': 'ID1'}, model_update) + + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + external_ref = {'source-id': 'ID1'} + model_update = self.driver.manage_existing_snapshot(test_snapshot, + external_ref) + self.assertEqual({'provider_location': 'ID1'}, model_update) + + @mock.patch.object(rest_client.RestClient, 'get_snapshot_info', + return_value={'ID': 'ID1', + 'EXPOSEDTOINITIATOR': 'false', + 'NAME': 'test1', + 'USERCAPACITY': 2097152, + 'PARENTID': '11', + 'HEALTHSTATUS': constants.STATUS_HEALTH}) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_id_by_name', + return_value='ID1') + def test_manage_existing_snapshot_mismatch_lun(self, mock_get_by_name, + mock_get_info): + external_ref = {'source-name': 'test1'} + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + ex = self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + test_snapshot, external_ref) + self.assertIsNotNone(re.search("Snapshot doesn't belong to volume", + ex.msg)) + + @mock.patch.object(rest_client.RestClient, 'get_snapshot_info', + return_value={'USERCAPACITY': 2097152}) + @mock.patch.object(rest_client.RestClient, 'get_snapshot_id_by_name', + return_value='ID1') + def test_manage_existing_snapshot_get_size_success(self, + mock_get_id_by_name, + mock_get_info): + external_ref = {'source-name': 'test1', + 'source-id': 'ID1'} + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + size = self.driver.manage_existing_snapshot_get_size(test_snapshot, + external_ref) + self.assertEqual(1, size) + + external_ref = {'source-name': 'test1'} + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + size = self.driver.manage_existing_snapshot_get_size(test_snapshot, + external_ref) + self.assertEqual(1, size) + + external_ref = {'source-id': 'ID1'} + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'volume': {'provider_location': '12'}} + size = self.driver.manage_existing_snapshot_get_size(test_snapshot, + external_ref) + self.assertEqual(1, size) + + @mock.patch.object(rest_client.RestClient, 'rename_snapshot') + def test_unmanage_snapshot(self, mock_rename): + test_snapshot = {'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635', + 'id': '21ec7341-9256-497b-97d9-ef48edcf0635'} + with mock.patch.object(rest_client.RestClient, + 'get_snapshot_id_by_name', + return_value=None): + self.driver.unmanage_snapshot(test_snapshot) + self.assertEqual(0, mock_rename.call_count) + + with mock.patch.object(rest_client.RestClient, + 'get_snapshot_id_by_name', + return_value='ID1'): + self.driver.unmanage_snapshot(test_snapshot) + self.assertEqual(1, mock_rename.call_count) + class FCSanLookupService(object): @@ -2236,7 +2389,7 @@ class HuaweiFCDriverTestCase(test.TestCase): def test_get_volume_status(self): data = self.driver.get_volume_stats() - self.assertEqual('2.0.2', data['driver_version']) + self.assertEqual('2.0.3', data['driver_version']) def test_extend_volume(self): diff --git a/cinder/volume/drivers/huawei/constants.py b/cinder/volume/drivers/huawei/constants.py index 213ae59ef..2618bce96 100644 --- a/cinder/volume/drivers/huawei/constants.py +++ b/cinder/volume/drivers/huawei/constants.py @@ -14,6 +14,7 @@ # under the License. STATUS_HEALTH = '1' +STATUS_ACTIVE = '43' STATUS_RUNNING = '10' STATUS_VOLUME_READY = '27' STATUS_LUNCOPY_READY = '40' diff --git a/cinder/volume/drivers/huawei/huawei_driver.py b/cinder/volume/drivers/huawei/huawei_driver.py index 5b541a8a7..276f0b2a6 100644 --- a/cinder/volume/drivers/huawei/huawei_driver.py +++ b/cinder/volume/drivers/huawei/huawei_driver.py @@ -1142,6 +1142,103 @@ class HuaweiBaseDriver(driver.VolumeDriver): raise exception.VolumeBackendAPIException(data=msg) return int(size) + def _check_snapshot_valid_for_manage(self, snapshot_info, external_ref): + snapshot_id = snapshot_info.get('ID') + + # Check whether the snapshot is normal. + if snapshot_info.get('HEALTHSTATUS') != constants.STATUS_HEALTH: + msg = _("Can't import snapshot %s to Cinder. " + "Snapshot status is not normal" + " or running status is not online.") % snapshot_id + raise exception.ManageExistingInvalidReference( + existing_ref=external_ref, reason=msg) + + if snapshot_info.get('EXPOSEDTOINITIATOR') != 'false': + msg = _("Can't import snapshot %s to Cinder. " + "Snapshot is exposed to initiator.") % snapshot_id + raise exception.ManageExistingInvalidReference( + existing_ref=external_ref, reason=msg) + + def _get_snapshot_info_by_ref(self, external_ref): + LOG.debug("Get snapshot external_ref: %s.", external_ref) + name = external_ref.get('source-name') + id = external_ref.get('source-id') + if not (name or id): + msg = _('Must specify snapshot source-name or source-id.') + raise exception.ManageExistingInvalidReference( + existing_ref=external_ref, reason=msg) + + snapshot_id = id or self.client.get_snapshot_id_by_name(name) + if not snapshot_id: + msg = _("Can't find snapshot on array, please check the " + "source-name or source-id.") + raise exception.ManageExistingInvalidReference( + existing_ref=external_ref, reason=msg) + + snapshot_info = self.client.get_snapshot_info(snapshot_id) + return snapshot_info + + def manage_existing_snapshot(self, snapshot, existing_ref): + snapshot_info = self._get_snapshot_info_by_ref(existing_ref) + snapshot_id = snapshot_info.get('ID') + volume = snapshot.get('volume') + lun_id = volume.get('provider_location') + if lun_id != snapshot_info.get('PARENTID'): + msg = (_("Can't import snapshot %s to Cinder. " + "Snapshot doesn't belong to volume."), snapshot_id) + raise exception.ManageExistingInvalidReference( + existing_ref=existing_ref, reason=msg) + + # Check whether this snapshot can be imported. + self._check_snapshot_valid_for_manage(snapshot_info, existing_ref) + + # Rename the snapshot to make it manageable for Cinder. + description = snapshot['id'] + snapshot_name = huawei_utils.encode_name(snapshot['id']) + self.client.rename_snapshot(snapshot_id, snapshot_name, description) + if snapshot_info.get('RUNNINGSTATUS') != constants.STATUS_ACTIVE: + self.client.activate_snapshot(snapshot_id) + + LOG.debug("Rename snapshot %(old_name)s to %(new_name)s.", + {'old_name': snapshot_info.get('NAME'), + 'new_name': snapshot_name}) + + return {'provider_location': snapshot_id} + + def manage_existing_snapshot_get_size(self, snapshot, existing_ref): + """Get the size of the existing snapshot.""" + snapshot_info = self._get_snapshot_info_by_ref(existing_ref) + size = (float(snapshot_info.get('USERCAPACITY')) + // constants.CAPACITY_UNIT) + remainder = (float(snapshot_info.get('USERCAPACITY')) + % constants.CAPACITY_UNIT) + if int(remainder) > 0: + msg = _("Snapshot size must be multiple of 1 GB.") + raise exception.VolumeBackendAPIException(data=msg) + return int(size) + + def unmanage_snapshot(self, snapshot): + """Unmanage the specified snapshot from Cinder management.""" + LOG.debug("Unmanage snapshot: %s.", snapshot['id']) + snapshot_name = huawei_utils.encode_name(snapshot['id']) + snapshot_id = self.client.get_snapshot_id_by_name(snapshot_name) + if not snapshot_id: + LOG.warning(_LW("Can't find snapshot on the array: %s."), + snapshot_name) + return + new_name = 'unmged_' + snapshot_name + LOG.debug("Rename snapshot %(snapshot_name)s to %(new_name)s.", + {'snapshot_name': snapshot_name, + 'new_name': new_name}) + + try: + self.client.rename_snapshot(snapshot_id, new_name) + except Exception: + LOG.warning(_LW("Failed to rename snapshot %(snapshot_id)s, " + "snapshot name on array is %(snapshot_name)s."), + {'snapshot_id': snapshot['id'], + 'snapshot_name': snapshot_name}) + class HuaweiISCSIDriver(HuaweiBaseDriver, driver.ISCSIDriver): """ISCSI driver for Huawei storage arrays. @@ -1159,9 +1256,10 @@ class HuaweiISCSIDriver(HuaweiBaseDriver, driver.ISCSIDriver): 2.0.0 - Rename to HuaweiISCSIDriver 2.0.1 - Manage/unmanage volume support 2.0.2 - Refactor HuaweiISCSIDriver + 2.0.3 - Manage/unmanage snapshot support """ - VERSION = "2.0.2" + VERSION = "2.0.3" def __init__(self, *args, **kwargs): super(HuaweiISCSIDriver, self).__init__(*args, **kwargs) @@ -1352,9 +1450,10 @@ class HuaweiFCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): 2.0.0 - Rename to HuaweiFCDriver 2.0.1 - Manage/unmanage volume support 2.0.2 - Refactor HuaweiFCDriver + 2.0.3 - Manage/unmanage snapshot support """ - VERSION = "2.0.2" + VERSION = "2.0.3" def __init__(self, *args, **kwargs): super(HuaweiFCDriver, self).__init__(*args, **kwargs) diff --git a/cinder/volume/drivers/huawei/rest_client.py b/cinder/volume/drivers/huawei/rest_client.py index c6b28ca49..495582c64 100644 --- a/cinder/volume/drivers/huawei/rest_client.py +++ b/cinder/volume/drivers/huawei/rest_client.py @@ -317,8 +317,12 @@ class RestClient(object): def get_snapshot_id_by_name(self, name): url = "/snapshot?range=[0-32767]" + description = 'The snapshot license file is unavailable.' result = self.call(url, None, "GET") - self._assert_rest_result(result, _('Get snapshot id error.')) + if 'error' in result: + if description == result['error']['description']: + return + self._assert_rest_result(result, _('Get snapshot id error.')) return self._get_id_from_result(result, name, 'NAME') @@ -1386,6 +1390,16 @@ class RestClient(object): return result['data'] + def get_snapshot_info(self, snapshot_id): + url = "/snapshot/" + snapshot_id + result = self.call(url, None, "GET") + + msg = _('Get snapshot error.') + self._assert_rest_result(result, msg) + self._assert_data_in_result(result, msg) + + return result['data'] + def extend_lun(self, lun_id, new_volume_size): url = "/lun/expand" data = {"TYPE": 11, "ID": lun_id, @@ -1621,6 +1635,16 @@ class RestClient(object): self._assert_rest_result(result, msg) self._assert_data_in_result(result, msg) + def rename_snapshot(self, snapshot_id, new_name, description=None): + url = "/snapshot/" + snapshot_id + data = {"NAME": new_name} + if description: + data.update({"DESCRIPTION": description}) + result = self.call(url, data, "PUT") + msg = _('Rename snapshot on array error.') + self._assert_rest_result(result, msg) + self._assert_data_in_result(result, msg) + def is_fc_initiator_associated_to_host(self, ininame): """Check whether the initiator is associated to the host.""" url = '/fc_initiator?range=[0-256]' diff --git a/releasenotes/notes/huawei-manage-unmanage-snapshot-e35ff844d72fedfb.yaml b/releasenotes/notes/huawei-manage-unmanage-snapshot-e35ff844d72fedfb.yaml new file mode 100644 index 000000000..54f8f7317 --- /dev/null +++ b/releasenotes/notes/huawei-manage-unmanage-snapshot-e35ff844d72fedfb.yaml @@ -0,0 +1,2 @@ +features: + - Add manage/unmanage snapshot support for Huawei drivers. \ No newline at end of file