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)
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):
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):
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.
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)
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)
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')
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,
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]'