]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Huawei: Add manage/unmanage snapshot support
authorchenzongliang <chenzongliang@huawei.com>
Sat, 12 Dec 2015 09:11:55 +0000 (17:11 +0800)
committerhuananhuawei <huanan@huawei.com>
Tue, 26 Jan 2016 01:34:21 +0000 (09:34 +0800)
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
cinder/volume/drivers/huawei/constants.py
cinder/volume/drivers/huawei/huawei_driver.py
cinder/volume/drivers/huawei/rest_client.py
releasenotes/notes/huawei-manage-unmanage-snapshot-e35ff844d72fedfb.yaml [new file with mode: 0644]

index 8e75023d467229f41f55310589f4c07a0bd11533..2f748565af89e66f7fe81686c718e2e02b516ffe 100644 (file)
@@ -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):
 
index 213ae59ef9ef73aa36f04ba26b1b92b9af2958aa..2618bce962a048cabe79c602a7db1bf2494cb6b9 100644 (file)
@@ -14,6 +14,7 @@
 #    under the License.
 
 STATUS_HEALTH = '1'
+STATUS_ACTIVE = '43'
 STATUS_RUNNING = '10'
 STATUS_VOLUME_READY = '27'
 STATUS_LUNCOPY_READY = '40'
index 5b541a8a7d30814220b8325375f32c6b140517d7..276f0b2a6ea52778d48701df6ef71dfb4f507aa5 100644 (file)
@@ -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)
index c6b28ca492351e130684c9175e852353cb3c67b0..495582c6465a3cc55fe297e860f50ec6520f2af7 100644 (file)
@@ -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 (file)
index 0000000..54f8f73
--- /dev/null
@@ -0,0 +1,2 @@
+features:
+  - Add manage/unmanage snapshot support for Huawei drivers.
\ No newline at end of file