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
This commit is contained in:
chenzongliang 2015-12-12 17:11:55 +08:00 committed by huananhuawei
parent 41f876f05e
commit 8d9f6be9de
5 changed files with 284 additions and 5 deletions

View 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):

View 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'

View 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)

View 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]'

View File

@ -0,0 +1,2 @@
features:
- Add manage/unmanage snapshot support for Huawei drivers.