Add support for manage/unmanage snapshots in HNAS driver

Adding support for manage/unmanage snapshots in Hitachi HNAS
driver. In order to manage a snapshot, the admin should provide the
snapshot size in "--driver-options" parameter.

Also, updating tempest tests for manage/unmanage snapshots to include
the required driver option.

DocImpact
Implements: blueprint hnas-manage-unmanage-snapshot-support

Change-Id: I93e56dda5cbe8d3dbe142d773f93d03a0c126d2f
This commit is contained in:
Alyson Rosa 2016-11-22 15:59:41 -02:00
parent 2ad181cffc
commit e02e16ea42
7 changed files with 257 additions and 16 deletions

View File

@ -53,7 +53,7 @@ Mapping of share drivers and share features support
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| HDFS | K | \- | M | \- | K | K | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Hitachi HNAS | L | L | L | M | L | L | \- |
| Hitachi HNAS | L | L | L | M | L | L | O |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Hitachi HSP | N | N | N | N | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+

View File

@ -358,14 +358,18 @@ class HitachiHNASDriver(driver.ShareDriver):
"""
hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
LOG.debug("The snapshot of share %(ss_sid)s will be created with "
"id %(ss_id)s.", {'ss_sid': snapshot['share_id'],
'ss_id': snapshot['id']})
LOG.debug("The snapshot of share %(snap_share_id)s will be created "
"with id %(snap_id)s.",
{'snap_share_id': snapshot['share_id'],
'snap_id': snapshot['id']})
self._create_snapshot(hnas_share_id, snapshot)
LOG.info(_LI("Snapshot %(id)s successfully created."),
{'id': snapshot['id']})
return {'provider_location': os.path.join('/snapshots', hnas_share_id,
snapshot['id'])}
def delete_snapshot(self, context, snapshot, share_server=None):
"""Deletes snapshot.
@ -375,12 +379,15 @@ class HitachiHNASDriver(driver.ShareDriver):
Not used by this driver.
"""
hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot)
LOG.debug("The snapshot %(ss_sid)s will be deleted. The related "
"share ID is %(ss_id)s.",
{'ss_sid': snapshot['share_id'], 'ss_id': snapshot['id']})
LOG.debug("The snapshot %(snap_id)s will be deleted. The related "
"share ID is %(snap_share_id)s.",
{'snap_id': snapshot['id'],
'snap_share_id': snapshot['share_id']})
self._delete_snapshot(hnas_share_id, hnas_snapshot_id)
self._delete_snapshot(hnas_share_id, snapshot['id'])
LOG.info(_LI("Snapshot %(id)s successfully deleted."),
{'id': snapshot['id']})
@ -432,9 +439,10 @@ class HitachiHNASDriver(driver.ShareDriver):
{'ss_id': snapshot['id']})
hnas_src_share_id = self._get_hnas_share_id(snapshot['share_id'])
hnas_src_snap_id = self._get_hnas_snapshot_id(snapshot)
export_list = self._create_share_from_snapshot(
share, hnas_src_share_id, snapshot)
share, hnas_src_share_id, hnas_src_snap_id)
LOG.debug("Share %(share)s created successfully on path(s): "
"%(paths)s.",
@ -707,6 +715,18 @@ class HitachiHNASDriver(driver.ShareDriver):
return hnas_id
def _get_hnas_snapshot_id(self, snapshot):
hnas_snapshot_id = snapshot['id']
if snapshot['provider_location']:
LOG.debug("Snapshot %(snap_id)s with provider_location: "
"%(p_loc)s.",
{'snap_id': hnas_snapshot_id,
'p_loc': snapshot['provider_location']})
hnas_snapshot_id = snapshot['provider_location'].split('/')[-1]
return hnas_snapshot_id
def _create_share(self, share_id, share_size, share_proto):
"""Creates share.
@ -914,7 +934,8 @@ class HitachiHNASDriver(driver.ShareDriver):
path = os.path.join('/snapshots', hnas_share_id)
self.hnas.delete_directory(path)
def _create_share_from_snapshot(self, share, src_hnas_share_id, snapshot):
def _create_share_from_snapshot(self, share, src_hnas_share_id,
hnas_snapshot_id):
"""Creates a new share from snapshot.
It copies everything from snapshot directory to a new vvol,
@ -922,14 +943,14 @@ class HitachiHNASDriver(driver.ShareDriver):
:param share: a dict from new share.
:param src_hnas_share_id: HNAS ID of share from which snapshot was
taken.
:param snapshot: a dict from snapshot that will be copied to
:param hnas_snapshot_id: HNAS ID from snapshot that will be copied to
new share.
:returns: Returns a list of dicts containing the new share's export
locations.
"""
dest_path = os.path.join('/shares', share['id'])
src_path = os.path.join('/snapshots', src_hnas_share_id,
snapshot['id'])
hnas_snapshot_id)
# Before copying everything to new vvol, we need to create it,
# because we only can transform an empty directory into a vvol.
@ -959,7 +980,7 @@ class HitachiHNASDriver(driver.ShareDriver):
msg = _LE('Failed to create share %(share_id)s from snapshot '
'%(snap)s.')
LOG.exception(msg, {'share_id': share['id'],
'snap': snapshot['id']})
'snap': hnas_snapshot_id})
self.hnas.vvol_delete(share['id'])
return self._get_export_locations(
@ -992,3 +1013,78 @@ class HitachiHNASDriver(driver.ShareDriver):
else:
export = r'\\%s\%s' % (ip, hnas_share_id)
return export
def manage_existing_snapshot(self, snapshot, driver_options):
"""Manages a snapshot that exists only in HNAS.
The snapshot to be managed should be in the path
/snapshots/SHARE_ID/SNAPSHOT_ID. Also, the size of snapshot should be
provided as --driver_options size=<size>.
:param snapshot: snapshot that will be managed.
:param driver_options: expects only one key 'size'. It must be
provided in order to manage a snapshot.
:returns: Returns a dict with size of snapshot managed
"""
try:
snapshot_size = int(driver_options.get("size", 0))
except (ValueError, TypeError):
msg = _("The size in driver options to manage snapshot "
"%(snap_id)s should be an integer, in format "
"driver-options size=<SIZE>. Value passed: "
"%(size)s.") % {'snap_id': snapshot['id'],
'size': driver_options.get("size")}
raise exception.ManageInvalidShareSnapshot(reason=msg)
if snapshot_size == 0:
msg = _("Snapshot %(snap_id)s has no size specified for manage. "
"Please, provide the size with parameter driver-options "
"size=<SIZE>.") % {'snap_id': snapshot['id']}
raise exception.ManageInvalidShareSnapshot(reason=msg)
hnas_share_id = self._get_hnas_share_id(snapshot['share_id'])
LOG.debug("Path provided to manage snapshot: %(path)s.",
{'path': snapshot['provider_location']})
path_info = snapshot['provider_location'].split('/')
if len(path_info) == 4 and path_info[1] == 'snapshots':
path_share_id = path_info[2]
hnas_snapshot_id = path_info[3]
else:
msg = (_("Incorrect path %(path)s for manage snapshot "
"%(snap_id)s. It should have the following format: "
"/snapshots/SHARE_ID/SNAPSHOT_ID.") %
{'path': snapshot['provider_location'],
'snap_id': snapshot['id']})
raise exception.ManageInvalidShareSnapshot(reason=msg)
if hnas_share_id != path_share_id:
msg = _("The snapshot %(snap_id)s does not belong to share "
"%(share_id)s.") % {'snap_id': snapshot['id'],
'share_id': snapshot['share_id']}
raise exception.ManageInvalidShareSnapshot(reason=msg)
if not self.hnas.check_snapshot(snapshot['provider_location']):
msg = _("Snapshot %(snap_id)s does not exist in "
"HNAS.") % {'snap_id': hnas_snapshot_id}
raise exception.ManageInvalidShareSnapshot(reason=msg)
LOG.info(_LI("Snapshot %(snap_path)s for share %(shr_id)s was "
"successfully managed with ID %(snap_id)s."),
{'snap_path': snapshot['provider_location'],
'shr_id': snapshot['share_id'], 'snap_id': snapshot['id']})
return {'size': snapshot_size}
def unmanage_snapshot(self, snapshot):
"""Unmanage a share snapshot
:param snapshot: Snapshot that will be unmanaged.
"""
LOG.info(_LI("The snapshot with ID %(snap_id)s from share "
"%(share_id)s is no longer being managed by Manila. "
"However, it is not deleted and can be found in HNAS."),
{'snap_id': snapshot['id'],
'share_id': snapshot['share_id']})

View File

@ -239,7 +239,7 @@ class HNASSSHBackend(object):
job_status.files_missing) == ("Job was completed",
"Success", '0', '0'):
LOG.debug("Snapshot of source path %(src)s to destination"
LOG.debug("Snapshot of source path %(src)s to destination "
"path %(dest)s created successfully.",
{'src': src_path,
'dest': dest_path})
@ -270,6 +270,20 @@ class HNASSSHBackend(object):
def delete_directory(self, path):
self._locked_selectfs('delete', path)
def check_snapshot(self, path):
command = ['path-to-object-number', '-f', self.fs_name, path]
try:
self._execute(command)
except processutils.ProcessExecutionError as e:
if 'Unable to locate component:' in e.stdout:
LOG.debug("Cannot find %(path)s: %(out)s",
{'path': path, 'out': e.stdout})
return False
else:
raise
return True
def check_fs_mounted(self):
command = ['df', '-a', '-f', self.fs_name]
output, err = self._execute(command)

View File

@ -93,12 +93,24 @@ snapshot_nfs = {
'id': 'abba6d9b-f29c-4bf7-aac1-618cda7aaf0f',
'share_id': 'aa4a7710-f326-41fb-ad18-b4ad587fc87a',
'share': share_nfs,
'provider_location': '/snapshots/aa4a7710-f326-41fb-ad18-b4ad587fc87a/'
'abba6d9b-f29c-4bf7-aac1-618cda7aaf0f',
}
snapshot_cifs = {
'id': '91bc6e1b-1ba5-f29c-abc1-da7618cabf0a',
'share_id': 'f5cadaf2-afbe-4cc4-9021-85491b6b76f7',
'share': share_cifs,
'provider_location': '/snapshots/f5cadaf2-afbe-4cc4-9021-85491b6b76f7/'
'91bc6e1b-1ba5-f29c-abc1-da7618cabf0a',
}
manage_snapshot = {
'id': 'bc168eb-fa71-beef-153a-3d451aa1351f',
'share_id': 'aa4a7710-f326-41fb-ad18-b4ad587fc87a',
'share': share_nfs,
'provider_location': '/snapshots/aa4a7710-f326-41fb-ad18-b4ad587fc87a'
'/snapshot18-05-2106',
}
invalid_share = {
@ -433,6 +445,9 @@ class HitachiHNASTestCase(test.TestCase):
@ddt.data(snapshot_nfs, snapshot_cifs)
def test_create_snapshot(self, snapshot):
hnas_id = snapshot['share_id']
p_location = {'provider_location': '/snapshots/' + hnas_id + '/' +
snapshot['id']}
self.mock_object(ssh.HNASSSHBackend, "get_nfs_host_list", mock.Mock(
return_value=['172.24.44.200(rw)']))
self.mock_object(ssh.HNASSSHBackend, "update_nfs_access_rule",
@ -441,11 +456,12 @@ class HitachiHNASTestCase(test.TestCase):
return_value=False))
self.mock_object(ssh.HNASSSHBackend, "tree_clone", mock.Mock())
self._driver.create_snapshot('context', snapshot)
out = self._driver.create_snapshot('context', snapshot)
ssh.HNASSSHBackend.tree_clone.assert_called_once_with(
'/shares/' + hnas_id, '/snapshots/' + hnas_id + '/' +
snapshot['id'])
self.assertEqual(p_location, out)
if snapshot['share']['share_proto'].lower() == 'nfs':
ssh.HNASSSHBackend.get_nfs_host_list.assert_called_once_with(
@ -516,6 +532,22 @@ class HitachiHNASTestCase(test.TestCase):
ssh.HNASSSHBackend.delete_directory.assert_called_once_with(
'/snapshots/' + hnas_id)
def test_delete_managed_snapshot(self):
hnas_id = manage_snapshot['share_id']
self.mock_object(driver.HitachiHNASDriver, "_check_fs_mounted")
self.mock_object(ssh.HNASSSHBackend, "tree_delete")
self.mock_object(ssh.HNASSSHBackend, "delete_directory")
self._driver.delete_snapshot('context', manage_snapshot)
self.assertTrue(self.mock_log.debug.called)
self.assertTrue(self.mock_log.info.called)
driver.HitachiHNASDriver._check_fs_mounted.assert_called_once_with()
ssh.HNASSSHBackend.tree_delete.assert_called_once_with(
manage_snapshot['provider_location'])
ssh.HNASSSHBackend.delete_directory.assert_called_once_with(
'/snapshots/' + hnas_id)
@ddt.data(share_nfs, share_cifs)
def test_ensure_share(self, share):
result = self._driver.ensure_share('context', share)
@ -817,3 +849,52 @@ class HitachiHNASTestCase(test.TestCase):
(manila.share.driver.ShareDriver._update_share_stats.
assert_called_once_with(fake_data))
self.assertTrue(self.mock_log.info.called)
def test_manage_existing_snapshot(self):
self.mock_object(ssh.HNASSSHBackend, 'check_snapshot',
mock.Mock(return_value=True))
out = self._driver.manage_existing_snapshot(manage_snapshot,
{'size': 20})
ssh.HNASSSHBackend.check_snapshot.assert_called_with(
'/snapshots/aa4a7710-f326-41fb-ad18-b4ad587fc87a'
'/snapshot18-05-2106')
self.assertEqual(20, out['size'])
self.assertTrue(self.mock_log.debug.called)
self.assertTrue(self.mock_log.info.called)
@ddt.data('fake_size', '128GB', '512 GB', {'size': 128})
def test_manage_snapshot_invalid_size_exception(self, size):
self.assertRaises(exception.ManageInvalidShareSnapshot,
self._driver.manage_existing_snapshot,
manage_snapshot, {'size': size})
def test_manage_snapshot_size_not_provided_exception(self):
self.assertRaises(exception.ManageInvalidShareSnapshot,
self._driver.manage_existing_snapshot,
manage_snapshot, {})
@ddt.data('/root/snapshot_id', '/snapshots/share1/snapshot_id',
'/directory1', 'snapshots/share1/snapshot_id')
def test_manage_snapshot_invalid_path_exception(self, path):
snap_copy = manage_snapshot.copy()
snap_copy['provider_location'] = path
self.assertRaises(exception.ManageInvalidShareSnapshot,
self._driver.manage_existing_snapshot,
snap_copy, {'size': 20})
self.assertTrue(self.mock_log.debug.called)
def test_manage_inexistent_snapshot_exception(self):
self.mock_object(ssh.HNASSSHBackend, 'check_snapshot',
mock.Mock(return_value=False))
self.assertRaises(exception.ManageInvalidShareSnapshot,
self._driver.manage_existing_snapshot,
manage_snapshot, {'size': 20})
self.assertTrue(self.mock_log.debug.called)
def test_unmanage_snapshot(self):
self._driver.unmanage_snapshot(snapshot_nfs)
self.assertTrue(self.mock_log.info.called)

View File

@ -466,6 +466,10 @@ X Allow Change & Read Unix user\\1090
"""
HNAS_RESULT_check_snap_error = """ \
path-to-object-number/FS-TestCG: Unable to locate component: share1
path-to-object-number/FS-TestCG: Failed to resolve object number"""
@ddt.ddt
class HNASSSHTestCase(test.TestCase):
@ -880,6 +884,46 @@ class HNASSSHTestCase(test.TestCase):
self._driver_ssh._locked_selectfs.assert_called_with(
*locked_selectfs_args)
def test_check_snapshot(self):
path = ("/snapshots/" + self.snapshot['share_id'] + "/" +
self.snapshot['id'])
check_snap_args = ['path-to-object-number', '-f', self.fs_name, path]
self.mock_object(ssh.HNASSSHBackend, '_execute')
out = self._driver_ssh.check_snapshot(path)
self.assertTrue(out)
self._driver_ssh._execute.assert_called_with(check_snap_args)
def test_check_inexistent_snapshot(self):
path = "/path/snap1/snapshot07-08-2016"
check_snap_args = ['path-to-object-number', '-f', self.fs_name, path]
self.mock_object(ssh.HNASSSHBackend, '_execute',
mock.Mock(side_effect=putils.ProcessExecutionError(
stdout=HNAS_RESULT_check_snap_error)))
out = self._driver_ssh.check_snapshot(path)
self.assertFalse(out)
self._driver_ssh._execute.assert_called_with(check_snap_args)
def test_check_snapshot_error(self):
path = "/path/snap1/snapshot07-08-2016"
check_snap_args = ['path-to-object-number', '-f', self.fs_name, path]
self.mock_object(ssh.HNASSSHBackend, '_execute',
mock.Mock(side_effect=putils.ProcessExecutionError(
stdout="Internal Server Error.")))
self.assertRaises(putils.ProcessExecutionError,
self._driver_ssh.check_snapshot, path)
self._driver_ssh._execute.assert_called_with(check_snap_args)
def test_check_fs_mounted_true(self):
self.mock_object(ssh.HNASSSHBackend, "_execute",
mock.Mock(return_value=(HNAS_RESULT_df, '')))

View File

@ -81,7 +81,10 @@ class ManageNFSSnapshotTest(base.BaseSharesAdminTest):
snapshot['provider_location'],
name=name,
description=description,
driver_options={},
# Some drivers require additional parameters passed as driver
# options, as follows:
# - size: Hitachi HNAS Driver
driver_options={'size': snapshot['size']},
version=version,
)

View File

@ -0,0 +1,3 @@
---
features:
- Added manage/unmanage snapshot support to Hitachi HNAS Driver.