From 70bfb78875de0bdda92ea2a482c3c1009bf33833 Mon Sep 17 00:00:00 2001 From: Alyson Rosa Date: Fri, 24 Jun 2016 10:53:09 -0300 Subject: [PATCH] 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 --- .../hitachi/test_hitachi_hnas_backend.py | 90 +++++++++++ .../drivers/hitachi/test_hitachi_hnas_nfs.py | 83 ++++++++++ cinder/volume/drivers/hitachi/hnas_backend.py | 59 ++++++++ cinder/volume/drivers/hitachi/hnas_nfs.py | 142 ++++++++++++++---- ...age-snapshot-support-40c8888cc594a7be.yaml | 3 + 5 files changed, 348 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/hnas-manage-unmanage-snapshot-support-40c8888cc594a7be.yaml diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_backend.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_backend.py index 0bd2b7ba14d..7827bf1c264 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_backend.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_backend.py @@ -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) diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_nfs.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_nfs.py index ecdb9e5a81f..8efc6190dfa 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_nfs.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hnas_nfs.py @@ -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) diff --git a/cinder/volume/drivers/hitachi/hnas_backend.py b/cinder/volume/drivers/hitachi/hnas_backend.py index 25af5091288..76ddc93451a 100644 --- a/cinder/volume/drivers/hitachi/hnas_backend.py +++ b/cinder/volume/drivers/hitachi/hnas_backend.py @@ -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() diff --git a/cinder/volume/drivers/hitachi/hnas_nfs.py b/cinder/volume/drivers/hitachi/hnas_nfs.py index fe72fb77a64..c393493f4d7 100644 --- a/cinder/volume/drivers/hitachi/hnas_nfs.py +++ b/cinder/volume/drivers/hitachi/hnas_nfs.py @@ -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}) diff --git a/releasenotes/notes/hnas-manage-unmanage-snapshot-support-40c8888cc594a7be.yaml b/releasenotes/notes/hnas-manage-unmanage-snapshot-support-40c8888cc594a7be.yaml new file mode 100644 index 00000000000..5c8298fb014 --- /dev/null +++ b/releasenotes/notes/hnas-manage-unmanage-snapshot-support-40c8888cc594a7be.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added manage/unmanage snapshot support to the HNAS NFS driver.