Merge "Add support for manage/unmanage snapshots in HNAS driver"
This commit is contained in:
commit
e8af5394b8
@ -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 | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
|
@ -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']})
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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, '')))
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added manage/unmanage snapshot support to Hitachi HNAS Driver.
|
Loading…
Reference in New Issue
Block a user