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.