HNAS: Add support for manage/unmanage snapshots in NFS driver

Added support for manage/unmanage snapshot in HNAS NFS driver.
This patch added functions that allow volume snapshots on HNAS
be managed by OpenStack, also, volume snapshots can be deleted
from OpenStack but still left in HNAS.

DocImpact
Implements: blueprint hnas-nfs-manage-unmanage-snapshot-support
Depends-On: I08175b031a65ea6ae5cec3c73d5312175f29c890

Change-Id: If06ecadeab814ff2f9420cee2e537ac0f71f0f9a
This commit is contained in:
Alyson Rosa 2016-06-24 10:53:09 -03:00
parent 83d7af85b3
commit 70bfb78875
5 changed files with 348 additions and 29 deletions

View File

@ -214,6 +214,31 @@ Logical units : No logical units. \n\
Access configuration: \n\
"
file_clone_stat = "Clone: /nfs_cinder/cinder-lu \n\
SnapshotFile: FileHandle[00000000004010000d20116826ffffffffffffff] \n\
\n\
SnapshotFile: FileHandle[00000000004029000d81f26826ffffffffffffff] \n\
"
file_clone_stat_snap_file1 = "\
FileHandle[00000000004010000d20116826ffffffffffffff] \n\n\
References: \n\
Clone: /nfs_cinder/cinder-lu \n\
Clone: /nfs_cinder/snapshot-lu-1 \n\
Clone: /nfs_cinder/snapshot-lu-2 \n\
"
file_clone_stat_snap_file2 = "\
FileHandle[00000000004010000d20116826ffffffffffffff] \n\n\
References: \n\
Clone: /nfs_cinder/volume-not-used \n\
Clone: /nfs_cinder/snapshot-1 \n\
Clone: /nfs_cinder/snapshot-2 \n\
"
not_a_clone = "\
file-clone-stat: failed to get predecessor snapshot-files: File is not a clone"
class HDSHNASBackendTest(test.TestCase):
@ -784,3 +809,68 @@ Thin ThinSize ThinAvail FS Type\n\
self.hnas_backend.create_target('cinder-default', 'fs-cinder',
'pxr6U37LZZJBoMc')
def test_check_snapshot_parent_true(self):
self.mock_object(self.hnas_backend, '_run_cmd',
mock.Mock(
side_effect=[(evsfs_list, ''),
(file_clone_stat, ''),
(file_clone_stat_snap_file1, ''),
(file_clone_stat_snap_file2, '')]))
out = self.hnas_backend.check_snapshot_parent('cinder-lu',
'snapshot-lu-1',
'fs-cinder')
self.assertTrue(out)
self.hnas_backend._run_cmd.assert_called_with('console-context',
'--evs', '2',
'file-clone-stat'
'-snapshot-file', '-f',
'fs-cinder',
'00000000004010000d2011'
'6826ffffffffffffff]')
def test_check_snapshot_parent_false(self):
self.mock_object(self.hnas_backend, '_run_cmd',
mock.Mock(
side_effect=[(evsfs_list, ''),
(file_clone_stat, ''),
(file_clone_stat_snap_file1, ''),
(file_clone_stat_snap_file2, '')]))
out = self.hnas_backend.check_snapshot_parent('cinder-lu',
'snapshot-lu-3',
'fs-cinder')
self.assertFalse(out)
self.hnas_backend._run_cmd.assert_called_with('console-context',
'--evs', '2',
'file-clone-stat'
'-snapshot-file', '-f',
'fs-cinder',
'00000000004029000d81f26'
'826ffffffffffffff]')
def test_check_a_not_cloned_file(self):
self.mock_object(self.hnas_backend, '_run_cmd',
mock.Mock(
side_effect=[(evsfs_list, ''),
(not_a_clone, '')]))
self.assertRaises(exception.ManageExistingInvalidReference,
self.hnas_backend.check_snapshot_parent,
'cinder-lu', 'snapshot-name', 'fs-cinder')
def test_get_export_path(self):
export_out = '/export01-husvm'
self.mock_object(self.hnas_backend, '_run_cmd',
mock.Mock(side_effect=[(evsfs_list, ''),
(nfs_export, '')]))
out = self.hnas_backend.get_export_path(export_out, 'fs-cinder')
self.assertEqual(export_out, out)
self.hnas_backend._run_cmd.assert_called_with('console-context',
'--evs', '2',
'nfs-export', 'list',
export_out)

View File

@ -501,3 +501,86 @@ class HNASNFSDriverTest(test.TestCase):
mock.Mock(side_effect=ValueError))
self.driver.unmanage(self.volume)
def test_manage_existing_snapshot(self):
nfs_share = "172.24.49.21:/fs-cinder"
nfs_mount = "/opt/stack/data/cinder/mnt/" + fake.SNAPSHOT_ID
path = "unmanage-snapshot-" + fake.SNAPSHOT_ID
loc = {'provider_location': '172.24.49.21:/fs-cinder'}
existing_ref = {'source-name': '172.24.49.21:/fs-cinder/'
+ fake.SNAPSHOT_ID}
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
mock.Mock(return_value=(nfs_share, nfs_mount, path)))
self.mock_object(backend.HNASSSHBackend, 'check_snapshot_parent',
mock.Mock(return_value=True))
self.mock_object(self.driver, '_execute')
self.mock_object(backend.HNASSSHBackend, 'get_export_path',
mock.Mock(return_value='fs-cinder'))
out = self.driver.manage_existing_snapshot(self.snapshot,
existing_ref)
self.assertEqual(loc, out)
def test_manage_existing_snapshot_not_parent_exception(self):
nfs_share = "172.24.49.21:/fs-cinder"
nfs_mount = "/opt/stack/data/cinder/mnt/" + fake.SNAPSHOT_ID
path = "unmanage-snapshot-" + fake.SNAPSHOT_ID
existing_ref = {'source-name': '172.24.49.21:/fs-cinder/'
+ fake.SNAPSHOT_ID}
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
mock.Mock(return_value=(nfs_share, nfs_mount, path)))
self.mock_object(backend.HNASSSHBackend, 'check_snapshot_parent',
mock.Mock(return_value=False))
self.mock_object(backend.HNASSSHBackend, 'get_export_path',
mock.Mock(return_value='fs-cinder'))
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot, self.snapshot,
existing_ref)
def test_manage_existing_snapshot_get_size(self):
existing_ref = {
'source-name': '172.24.49.21:/fs-cinder/cinder-snapshot',
}
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
expected_size = 1
self.mock_object(self.driver, '_ensure_shares_mounted')
self.mock_object(utils, 'resolve_hostname',
mock.Mock(return_value='172.24.49.21'))
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
mock.Mock(return_value='/mnt/silver'))
self.mock_object(os.path, 'isfile',
mock.Mock(return_value=True))
self.mock_object(utils, 'get_file_size',
mock.Mock(return_value=expected_size))
out = self.driver.manage_existing_snapshot_get_size(
self.snapshot, existing_ref)
self.assertEqual(1, out)
utils.get_file_size.assert_called_once_with(
'/mnt/silver/cinder-snapshot')
utils.resolve_hostname.assert_called_with('172.24.49.21')
def test_unmanage_snapshot(self):
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
snapshot_name = 'snapshot-' + self.snapshot.id
old_path = os.path.join(path, snapshot_name)
new_path = os.path.join(path, 'unmanage-' + snapshot_name)
self.mock_object(self.driver, '_get_mount_point_for_share',
mock.Mock(return_value=path))
self.mock_object(self.driver, '_execute')
self.driver.unmanage_snapshot(self.snapshot)
self.driver._execute.assert_called_with('mv', old_path, new_path,
run_as_root=False,
check_exit_code=True)
self.driver._get_mount_point_for_share.assert_called_with(
self.snapshot.provider_location)

View File

@ -813,3 +813,62 @@ class HNASSSHBackend(object):
self._get_targets(_evs_id, refresh=True)
LOG.debug("create_target: alias: %(alias)s fs_label: %(fs_label)s",
{'alias': tgt_alias, 'fs_label': fs_label})
def _get_file_handler(self, volume_path, _evs_id, fs_label):
out, err = self._run_cmd("console-context", "--evs", _evs_id,
'file-clone-stat', '-f', fs_label,
volume_path)
if "File is not a clone" in out:
msg = (_("%s is not a clone!"), volume_path)
raise exception.ManageExistingInvalidReference(
existing_ref=volume_path, reason=msg)
lines = out.split('\n')
filehandle_list = []
for line in lines:
if "SnapshotFile:" in line and "FileHandle" in line:
item = line.split(':')
handler = item[1][:-1].replace(' FileHandle[', "")
filehandle_list.append(handler)
LOG.debug("Volume handler found: %(fh)s. Adding to list...",
{'fh': handler})
return filehandle_list
def check_snapshot_parent(self, volume_path, snap_name, fs_label):
_evs_id = self.get_evs(fs_label)
file_handler_list = self._get_file_handler(volume_path, _evs_id,
fs_label)
for file_handler in file_handler_list:
out, err = self._run_cmd("console-context", "--evs", _evs_id,
'file-clone-stat-snapshot-file',
'-f', fs_label, file_handler)
lines = out.split('\n')
for line in lines:
if snap_name in line:
LOG.debug("Snapshot %(snap)s found in children list from "
"%(vol)s!", {'snap': snap_name,
'vol': volume_path})
return True
LOG.debug("Snapshot %(snap)s was not found in children list from "
"%(vol)s, probably it is not the parent!",
{'snap': snap_name, 'vol': volume_path})
return False
def get_export_path(self, export, fs_label):
evs_id = self.get_evs(fs_label)
out, err = self._run_cmd("console-context", "--evs", evs_id,
'nfs-export', 'list', export)
lines = out.split('\n')
for line in lines:
if 'Export path:' in line:
return line.split('Export path:')[1].strip()

View File

@ -79,6 +79,7 @@ class HNASNFSDriver(nfs.NfsDriver):
Updated to use versioned objects
Changed the class name to HNASNFSDriver
Deprecated XML config file
Added support to manage/unmanage snapshots features
"""
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Hitachi_HNAS_CI"
@ -494,7 +495,8 @@ class HNASNFSDriver(nfs.NfsDriver):
raise exception.ManageExistingInvalidReference(
existing_ref=vol_ref,
reason=_('Volume not found on configured storage backend.'))
reason=_('Volume/Snapshot not found on configured storage '
'backend.'))
@cutils.trace
def manage_existing(self, volume, existing_vol_ref):
@ -590,33 +592,7 @@ class HNASNFSDriver(nfs.NfsDriver):
:returns: the size of the volume or raise error
:raises: VolumeBackendAPIException
"""
# Attempt to find NFS share, NFS mount, and volume path from vol_ref.
(nfs_share, nfs_mount, vol_name
) = self._get_share_mount_and_vol_from_vol_ref(existing_vol_ref)
LOG.debug("Asked to get size of NFS vol_ref %(ref)s.",
{'ref': existing_vol_ref['source-name']})
if utils.check_already_managed_volume(vol_name):
raise exception.ManageExistingAlreadyManaged(volume_ref=vol_name)
try:
file_path = os.path.join(nfs_mount, vol_name)
file_size = float(cutils.get_file_size(file_path)) / units.Gi
vol_size = int(math.ceil(file_size))
except (OSError, ValueError):
exception_message = (_("Failed to manage existing volume "
"%(name)s, because of error in getting "
"volume size."),
{'name': existing_vol_ref['source-name']})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
LOG.debug("Reporting size of NFS volume ref %(ref)s as %(size)d GB.",
{'ref': existing_vol_ref['source-name'], 'size': vol_size})
return vol_size
return self._manage_existing_get_size(existing_vol_ref)
@cutils.trace
def unmanage(self, volume):
@ -647,4 +623,112 @@ class HNASNFSDriver(nfs.NfsDriver):
except (OSError, ValueError):
LOG.exception(_LE("The NFS Volume %(cr)s does not exist."),
{'cr': vol_path})
{'cr': new_path})
def _manage_existing_get_size(self, existing_ref):
# Attempt to find NFS share, NFS mount, and path from vol_ref.
(nfs_share, nfs_mount, path
) = self._get_share_mount_and_vol_from_vol_ref(existing_ref)
try:
LOG.debug("Asked to get size of NFS ref %(ref)s.",
{'ref': existing_ref['source-name']})
file_path = os.path.join(nfs_mount, path)
file_size = float(cutils.get_file_size(file_path)) / units.Gi
# Round up to next Gb
size = int(math.ceil(file_size))
except (OSError, ValueError):
exception_message = (_("Failed to manage existing volume/snapshot "
"%(name)s, because of error in getting "
"its size."),
{'name': existing_ref['source-name']})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
LOG.debug("Reporting size of NFS ref %(ref)s as %(size)d GB.",
{'ref': existing_ref['source-name'], 'size': size})
return size
def _check_snapshot_parent(self, volume, old_snap_name, share):
volume_name = 'volume-' + volume.id
(fs, path, fs_label) = self._get_service(volume)
# 172.24.49.34:/nfs_cinder
export_path = self.backend.get_export_path(share.split(':')[1],
fs_label)
volume_path = os.path.join(export_path, volume_name)
return self.backend.check_snapshot_parent(volume_path, old_snap_name,
fs_label)
def manage_existing_snapshot(self, snapshot, existing_ref):
# Attempt to find NFS share, NFS mount, and volume path from ref.
(nfs_share, nfs_mount, src_snapshot_name
) = self._get_share_mount_and_vol_from_vol_ref(existing_ref)
LOG.info(_LI("Asked to manage NFS snapshot %(snap)s for volume "
"%(vol)s, with vol ref %(ref)s."),
{'snap': snapshot.id,
'vol': snapshot.volume_id,
'ref': existing_ref['source-name']})
volume = snapshot.volume
# Check if the snapshot belongs to the volume
real_parent = self._check_snapshot_parent(volume, src_snapshot_name,
nfs_share)
if not real_parent:
msg = (_("This snapshot %(snap)s doesn't belong "
"to the volume parent %(vol)s.") %
{'snap': snapshot.id, 'vol': volume.id})
raise exception.ManageExistingInvalidReference(
existing_ref=existing_ref, reason=msg)
if src_snapshot_name == snapshot.name:
LOG.debug("New Cinder snapshot %(snap)s name matches reference "
"name. No need to rename.", {'snap': snapshot.name})
else:
src_snap = os.path.join(nfs_mount, src_snapshot_name)
dst_snap = os.path.join(nfs_mount, snapshot.name)
try:
self._try_execute("mv", src_snap, dst_snap, run_as_root=False,
check_exit_code=True)
LOG.info(_LI("Setting newly managed Cinder snapshot name "
"to %(snap)s."), {'snap': snapshot.name})
self._set_rw_permissions_for_all(dst_snap)
except (OSError, processutils.ProcessExecutionError) as err:
msg = (_("Failed to manage existing snapshot "
"%(name)s, because rename operation "
"failed: Error msg: %(msg)s.") %
{'name': existing_ref['source-name'],
'msg': six.text_type(err)})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return {'provider_location': nfs_share}
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
return self._manage_existing_get_size(existing_ref)
def unmanage_snapshot(self, snapshot):
path = self._get_mount_point_for_share(snapshot.provider_location)
new_name = "unmanage-" + snapshot.name
old_path = os.path.join(path, snapshot.name)
new_path = os.path.join(path, new_name)
try:
self._execute("mv", old_path, new_path,
run_as_root=False, check_exit_code=True)
LOG.info(_LI("The snapshot with path %(old)s is no longer being "
"managed by Cinder. However, it was not deleted and "
"can be found in the new path %(cr)s."),
{'old': old_path, 'cr': new_path})
except (OSError, ValueError):
LOG.exception(_LE("The NFS snapshot %(old)s does not exist."),
{'old': old_path})

View File

@ -0,0 +1,3 @@
---
features:
- Added manage/unmanage snapshot support to the HNAS NFS driver.