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:
parent
83d7af85b3
commit
70bfb78875
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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})
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added manage/unmanage snapshot support to the HNAS NFS driver.
|
Loading…
Reference in New Issue
Block a user