From a5ce771687894de02f3f068c73c779a8027a8a9d Mon Sep 17 00:00:00 2001 From: Ivan Pchelintsev Date: Wed, 16 Jun 2021 13:20:10 +0300 Subject: [PATCH] Add Cinder NFS driver for Dell PowerStore Implements: blueprint powerstore-nfs-driver Co-Authored-By: Alexander Malashenko Change-Id: Ide1d002acb8e1730767b15afc0566b2bb25999ed --- cinder/opts.py | 3 + .../drivers/dell_emc/powerstore/test_nfs.py | 462 ++++++++++++++++++ .../volume/drivers/dell_emc/powerstore/nfs.py | 238 +++++++++ .../drivers/dell-emc-powerstore-nfs.rst | 61 +++ doc/source/reference/support-matrix.ini | 13 + etc/cinder/rootwrap.d/volume.filters | 3 + ...re-nfs-cinder-driver-b743a8a89acafa35.yaml | 4 + 7 files changed, 784 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_nfs.py create mode 100644 cinder/volume/drivers/dell_emc/powerstore/nfs.py create mode 100644 doc/source/configuration/block-storage/drivers/dell-emc-powerstore-nfs.rst create mode 100644 releasenotes/notes/bp-powerstore-nfs-cinder-driver-b743a8a89acafa35.yaml diff --git a/cinder/opts.py b/cinder/opts.py index cf3b79057c3..7a20c078b7b 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -80,6 +80,8 @@ from cinder.volume.drivers.dell_emc.powermax import common as \ cinder_volume_drivers_dell_emc_powermax_common from cinder.volume.drivers.dell_emc.powerstore import driver as \ cinder_volume_drivers_dell_emc_powerstore_driver +from cinder.volume.drivers.dell_emc.powerstore import nfs as \ + cinder_volume_drivers_dell_emc_powerstore_nfs from cinder.volume.drivers.dell_emc.powervault import common as \ cinder_volume_drivers_dell_emc_powervault_common from cinder.volume.drivers.dell_emc.sc import storagecenter_common as \ @@ -324,6 +326,7 @@ def list_opts(): cinder_volume_drivers_dell_emc_powermax_common.powermax_opts, cinder_volume_drivers_dell_emc_powerstore_driver. POWERSTORE_OPTS, + cinder_volume_drivers_dell_emc_powerstore_nfs.nfs_opts, cinder_volume_drivers_dell_emc_powervault_common.common_opts, cinder_volume_drivers_dell_emc_powervault_common.iscsi_opts, cinder_volume_drivers_dell_emc_sc_storagecentercommon. diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_nfs.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_nfs.py new file mode 100644 index 00000000000..79a80b246b8 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_nfs.py @@ -0,0 +1,462 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import errno +import os +from unittest import mock + +import ddt +from oslo_concurrency import processutils as putils +from oslo_utils import imageutils +from oslo_utils import units + +from cinder import context +from cinder import exception +from cinder.image import image_utils +from cinder.tests.unit import fake_constants as fake +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume +from cinder.tests.unit import test +from cinder.volume import configuration as conf +from cinder.volume.drivers.dell_emc.powerstore import nfs +from cinder.volume import volume_utils + + +NFS_CONFIG = {'max_over_subscription_ratio': 1.0, + 'reserved_percentage': 0, + 'nfs_sparsed_volumes': True, + 'nfs_qcow2_volumes': False, + 'nas_secure_file_permissions': 'false', + 'nas_secure_file_operations': 'false'} + + +QEMU_IMG_INFO_OUT1 = """image: %(volid)s + file format: raw + virtual size: %(size_gb)sG (%(size_b)s bytes) + disk size: 173K + """ + +QEMU_IMG_INFO_OUT2 = """image: %(volid)s + file format: qcow2 + virtual size: %(size_gb)sG (%(size_b)s bytes) + disk size: 173K + """ + +QEMU_IMG_INFO_OUT3 = """image: volume-%(volid)s.%(snapid)s + file format: qcow2 + virtual size: %(size_gb)sG (%(size_b)s bytes) + disk size: 196K + cluster_size: 65536 + backing file: volume-%(volid)s + backing file format: qcow2 + Format specific information: + compat: 1.1 + lazy refcounts: false + refcount bits: 16 + corrupt: false + """ + + +@ddt.ddt +class PowerStoreNFSDriverInitializeTestCase(test.TestCase): + TEST_NFS_HOST = 'nfs-host1' + + def setUp(self): + super(PowerStoreNFSDriverInitializeTestCase, self).setUp() + self.context = mock.Mock() + self.create_configuration() + self.override_config('compute_api_class', 'unittest.mock.Mock') + self.drv = nfs.PowerStoreNFSDriverInitialization( + configuration=self.configuration) + + def create_configuration(self): + config = conf.Configuration(None) + config.append_config_values(nfs.nfs_opts) + self.configuration = config + + def test_check_multiattach_support(self): + drv = self.drv + + self.configuration.nfs_qcow2_volumes = False + + drv._check_multiattach_support() + self.assertEqual(not self.configuration.nfs_qcow2_volumes, + drv.multiattach_support) + + def test_check_multiattach_support_disable(self): + drv = self.drv + drv.configuration.nfs_qcow2_volumes = True + + drv._check_multiattach_support() + self.assertEqual(not self.configuration.nfs_qcow2_volumes, + drv.multiattach_support) + + def test_check_snapshot_support(self): + drv = self.drv + drv.configuration.nfs_snapshot_support = True + drv.configuration.nas_secure_file_operations = 'false' + + drv._check_snapshot_support() + + self.assertTrue(drv.configuration.nfs_snapshot_support) + + def test_check_snapshot_support_disable(self): + drv = self.drv + drv.configuration.nfs_snapshot_support = False + drv.configuration.nas_secure_file_operations = 'false' + + self.assertRaises(exception.VolumeDriverException, + drv._check_snapshot_support) + + def test_check_snapshot_support_nas_true(self): + drv = self.drv + drv.configuration.nfs_snapshot_support = True + drv.configuration.nas_secure_file_operations = 'true' + + self.assertRaises(exception.VolumeDriverException, + drv._check_snapshot_support) + + @mock.patch("cinder.volume.drivers.nfs.NfsDriver.do_setup") + def test_do_setup(self, mock_super_do_setup): + drv = self.drv + drv.configuration.nas_host = self.TEST_NFS_HOST + + mock_check_multiattach_support = self.mock_object( + drv, '_check_multiattach_support' + ) + + drv.do_setup(self.context) + + self.assertTrue(mock_check_multiattach_support.called) + + def test_check_package_is_installed(self): + drv = self.drv + package = 'dellfcopy' + mock_execute = self.mock_object(drv, '_execute') + drv._check_package_is_installed(package) + mock_execute.assert_called_once_with(package, + check_exit_code=False, + run_as_root=False) + + def test_check_package_is_not_installed(self): + drv = self.drv + package = 'dellfcopy' + drv._execute = mock.Mock( + side_effect=OSError( + errno.ENOENT, 'No such file or directory' + ) + ) + self.assertRaises(exception.VolumeDriverException, + drv._check_package_is_installed, package) + drv._execute.assert_called_once_with(package, + check_exit_code=False, + run_as_root=False) + + def test_check_for_setup_error(self): + drv = self.drv + mock_check_package_is_installed = self.mock_object( + drv, '_check_package_is_installed') + drv.check_for_setup_error() + mock_check_package_is_installed.assert_called_once_with('dellfcopy') + + def test_check_for_setup_error_not_passed(self): + drv = self.drv + drv._execute = mock.Mock( + side_effect=OSError( + errno.ENOENT, 'No such file or directory' + ) + ) + self.assertRaises(exception.VolumeDriverException, + drv.check_for_setup_error) + drv._execute.assert_called_once_with('dellfcopy', + check_exit_code=False, + run_as_root=False) + + def test_update_volume_stats_has_multiattach(self): + drv = self.drv + + self.mock_object(nfs.NfsDriver, '_update_volume_stats') + drv.multiattach_support = True + drv._stats = {} + + drv._update_volume_stats() + + self.assertIn('multiattach', drv._stats) + self.assertTrue(drv._stats['multiattach']) + + +@ddt.ddt +class PowerStoreNFSDriverTestCase(test.TestCase): + TEST_NFS_HOST = 'nfs-host1' + TEST_NFS_SHARE_PATH = '/export' + TEST_NFS_EXPORT = '%s:%s' % (TEST_NFS_HOST, TEST_NFS_SHARE_PATH) + TEST_SIZE_IN_GB = 1 + TEST_MNT_POINT = '/mnt/nfs' + TEST_MNT_POINT_BASE_EXTRA_SLASH = '/opt/stack/data/cinder//mnt' + TEST_MNT_POINT_BASE = '/mnt/test' + TEST_LOCAL_PATH = '/mnt/nfs/volume-123' + TEST_FILE_NAME = 'test.txt' + VOLUME_UUID = 'abcdefab-cdef-abcd-efab-cdefabcdefab' + + def setUp(self): + super(PowerStoreNFSDriverTestCase, self).setUp() + + self.configuration = mock.Mock(conf.Configuration) + self.configuration.append_config_values(mock.ANY) + self.configuration.nfs_sparsed_volumes = True + self.configuration.nas_secure_file_permissions = 'false' + self.configuration.nas_secure_file_operations = 'false' + self.configuration.nfs_mount_point_base = self.TEST_MNT_POINT_BASE + self.configuration.nfs_snapshot_support = True + self.configuration.max_over_subscription_ratio = 1.0 + self.configuration.reserved_percentage = 5 + self.configuration.nfs_mount_options = None + self.configuration.nfs_qcow2_volumes = True + self.configuration.nas_host = '0.0.0.0' + self.configuration.nas_share_path = None + + self.mock_object(volume_utils, 'get_max_over_subscription_ratio', + return_value=1) + self.context = context.get_admin_context() + + self._driver = nfs.PowerStoreNFSDriver( + configuration=self.configuration) + self._driver.shares = {} + self.mock_object(self._driver, '_execute') + + def test_do_fast_clone_file(self): + drv = self._driver + volume_path = 'fake/path' + new_volume_path = 'fake/new_path' + + drv._do_fast_clone_file(volume_path, new_volume_path) + + drv._execute.assert_called_once_with( + 'dellfcopy', '-o', 'fastclone', '-s', volume_path, '-d', + new_volume_path, '-v', '1', run_as_root=True + ) + + def test_do_fast_clone_file_raise_error(self): + drv = self._driver + volume_path = 'fake/path' + new_volume_path = 'fake/new_path' + + drv._execute = mock.Mock( + side_effect=putils.ProcessExecutionError() + ) + self.assertRaises(putils.ProcessExecutionError, + drv._do_fast_clone_file, volume_path, + new_volume_path) + drv._execute.assert_called_once_with( + 'dellfcopy', '-o', 'fastclone', '-s', volume_path, '-d', + new_volume_path, '-v', '1', run_as_root=True + ) + + def _simple_volume(self, **kwargs): + updates = {'id': self.VOLUME_UUID, + 'provider_location': self.TEST_NFS_EXPORT, + 'display_name': f'volume-{self.VOLUME_UUID}', + 'name': f'volume-{self.VOLUME_UUID}', + 'size': 10, + 'status': 'available'} + + updates.update(kwargs) + if 'display_name' not in updates: + updates['display_name'] = 'volume-%s' % updates['id'] + + return fake_volume.fake_volume_obj(self.context, **updates) + + def test_delete_volume_without_info(self): + drv = self._driver + volume = fake_volume.fake_volume_obj( + self.context, + display_name='volume', + provider_location=self.TEST_NFS_EXPORT + ) + vol_path = '/path/to/vol' + + mock_ensure_share_mounted = self.mock_object( + drv, '_ensure_share_mounted') + mock_local_path_volume_info = self.mock_object( + drv, '_local_path_volume_info' + ) + mock_local_path_volume_info.return_value = self.TEST_LOCAL_PATH + mock_read_info_file = self.mock_object(drv, '_read_info_file') + mock_read_info_file.return_value = {} + mock_local_path_volume = self.mock_object(drv, '_local_path_volume') + mock_local_path_volume.return_value = vol_path + + drv.delete_volume(volume) + + mock_ensure_share_mounted.assert_called_once_with( + self.TEST_NFS_EXPORT) + mock_local_path_volume.assert_called_once_with(volume) + mock_read_info_file.assert_called_once_with( + self.TEST_LOCAL_PATH, empty_if_missing=True) + mock_local_path_volume.assert_called_once_with(volume) + drv._execute.assert_called_once_with( + 'rm', '-f', vol_path, run_as_root=True) + + def test_delete_volume_with_info(self): + drv = self._driver + volume = fake_volume.fake_volume_obj( + self.context, + display_name='volume', + provider_location=self.TEST_NFS_EXPORT + ) + vol_path = '/path/to/vol' + with mock.patch.object(drv, '_ensure_share_mounted'): + mock_local_path_volume_info = self.mock_object( + drv, '_local_path_volume_info' + ) + mock_local_path_volume_info.return_value = self.TEST_LOCAL_PATH + mock_read_info_file = self.mock_object(drv, '_read_info_file') + mock_read_info_file.return_value = {'active': '/path/to/active'} + mock_local_path_volume = self.mock_object( + drv, '_local_path_volume') + mock_local_path_volume.return_value = vol_path + + drv.delete_volume(volume) + + self.assertEqual(drv._execute.call_count, 3) + + def test_delete_volume_without_provider_location(self): + drv = self._driver + volume = fake_volume.fake_volume_obj( + self.context, + display_name='volume', + provider_location='' + ) + drv.delete_volume(volume) + + self.assertFalse(bool(drv._execute.call_count)) + + @ddt.data([None, QEMU_IMG_INFO_OUT1], + ['raw', QEMU_IMG_INFO_OUT1], + ['qcow2', QEMU_IMG_INFO_OUT2]) + @ddt.unpack + @mock.patch('cinder.objects.volume.Volume.get_by_id') + def test_extend_volume(self, file_format, qemu_img_info, mock_get): + drv = self._driver + volume = fake_volume.fake_volume_obj( + self.context, + id='80ee16b6-75d2-4d54-9539-ffc1b4b0fb10', + size=1, + provider_location='nfs_share') + if file_format: + volume.admin_metadata = {'format': file_format} + mock_get.return_value = volume + path = 'path' + new_size = volume['size'] + 1 + + mock_img_utils = self.mock_object(drv, '_qemu_img_info') + img_out = qemu_img_info % {'volid': volume.id, + 'size_gb': volume.size, + 'size_b': volume.size * units.Gi} + mock_img_utils.return_value = imageutils.QemuImgInfo( + img_out) + + with mock.patch.object(image_utils, 'resize_image') as resize: + with mock.patch.object(drv, 'local_path', return_value=path): + with mock.patch.object(drv, '_is_share_eligible', + return_value=True): + + drv.extend_volume(volume, new_size) + + resize.assert_called_once_with(path, new_size) + + def test_create_volume_from_snapshot(self): + drv = self._driver + src_volume = self._simple_volume(size=10) + src_volume.id = fake.VOLUME_ID + + fake_snap = fake_snapshot.fake_snapshot_obj(self.context) + fake_snap.volume = src_volume + fake_snap.size = 10 + fake_snap.status = 'available' + + new_volume = self._simple_volume(size=src_volume.size) + + drv._find_share = mock.Mock(return_value=self.TEST_NFS_EXPORT) + drv._copy_volume_from_snapshot = mock.Mock() + + drv._create_volume_from_snapshot(new_volume, fake_snap) + + drv._find_share.assert_called_once_with(new_volume) + drv._copy_volume_from_snapshot.assert_called_once_with( + fake_snap, new_volume, new_volume.size + ) + + @mock.patch('cinder.objects.volume.Volume.get_by_id') + def test_create_cloned_volume(self, mock_get): + drv = self._driver + volume = self._simple_volume() + mock_get.return_value = volume + vol_dir = os.path.join(self.TEST_MNT_POINT_BASE, + drv._get_hash_str(volume.provider_location)) + vol_path = os.path.join(vol_dir, volume.name) + + new_volume = self._simple_volume() + new_vol_dir = os.path.join(self.TEST_MNT_POINT_BASE, + drv._get_hash_str( + volume.provider_location)) + new_vol_path = os.path.join(new_vol_dir, volume.name) + + drv._create_cloned_volume(new_volume, volume, self.context) + + command = ['dellfcopy', '-o', 'fastclone', '-s', vol_path, + '-d', new_vol_path, '-v', '1'] + calls = [mock.call(*command, run_as_root=True)] + drv._execute.assert_has_calls(calls) + + @ddt.data([QEMU_IMG_INFO_OUT3]) + @ddt.unpack + @mock.patch('cinder.objects.volume.Volume.save') + def test_copy_volume_from_snapshot(self, qemu_img_info, mock_save): + drv = self._driver + src_volume = self._simple_volume(size=10) + src_volume.id = fake.VOLUME_ID + + fake_snap = fake_snapshot.fake_snapshot_obj(self.context) + snap_file = src_volume.name + '.' + fake_snap.id + fake_snap.volume = src_volume + fake_snap.size = 10 + fake_source_vol_path = os.path.join( + drv._local_volume_dir(fake_snap.volume), + src_volume.name + ) + + new_volume = self._simple_volume(size=10) + new_vol_dir = os.path.join(self.TEST_MNT_POINT_BASE, + drv._get_hash_str( + src_volume.provider_location)) + new_vol_path = os.path.join(new_vol_dir, new_volume.name) + + mock_read_info_file = self.mock_object(drv, '_read_info_file') + mock_read_info_file.return_value = {'active': snap_file, + fake_snap.id: snap_file} + + mock_img_utils = self.mock_object(drv, '_qemu_img_info') + img_out = qemu_img_info % {'volid': src_volume.id, + 'snapid': fake_snap.id, + 'size_gb': src_volume.size, + 'size_b': src_volume.size * units.Gi} + mock_img_utils.return_value = imageutils.QemuImgInfo(img_out) + + drv._copy_volume_from_snapshot(fake_snap, new_volume, new_volume.size) + + command = ['dellfcopy', '-o', 'fastclone', '-s', fake_source_vol_path, + '-d', new_vol_path, '-v', '1'] + calls = [mock.call(*command, run_as_root=True)] + drv._execute.assert_has_calls(calls) diff --git a/cinder/volume/drivers/dell_emc/powerstore/nfs.py b/cinder/volume/drivers/dell_emc/powerstore/nfs.py new file mode 100644 index 00000000000..a15d9a506cc --- /dev/null +++ b/cinder/volume/drivers/dell_emc/powerstore/nfs.py @@ -0,0 +1,238 @@ +# Copyright (c) 2020 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import collections +import errno +import os + +from oslo_concurrency import processutils as putils +from oslo_config import cfg +from oslo_log import log as logging + +from cinder import coordination +from cinder import exception +from cinder.i18n import _ +from cinder.image import image_utils +from cinder import interface +from cinder.volume import configuration +from cinder.volume.drivers.nfs import nfs_opts +from cinder.volume.drivers.nfs import NfsDriver + + +LOG = logging.getLogger(__name__) + + +CONF = cfg.CONF +CONF.register_opts(nfs_opts, group=configuration.SHARED_CONF_GROUP) + + +class PowerStoreNFSDriverInitialization(NfsDriver): + """Implementation of PowerStoreNFSDriver initialization. + + Added multiattach support option and checking that the required + packages are installed. + """ + driver_volume_type = 'nfs' + driver_prefix = 'nfs' + volume_backend_name = 'PowerStore_NFS' + VERSION = '1.0.0' + VENDOR = 'Dell' + + # ThirdPartySystems wiki page + CI_WIKI_NAME = "DellEMC_PowerStore_CI" + + def __init__(self, execute=putils.execute, *args, **kwargs): + super(PowerStoreNFSDriverInitialization, self).__init__( + execute, *args, **kwargs) + self.multiattach_support = False + + def do_setup(self, context): + super(PowerStoreNFSDriverInitialization, self).do_setup(context) + self._check_multiattach_support() + + def _check_multiattach_support(self): + """Enable multiattach support if nfs_qcow2_volumes disabled.""" + + self.multiattach_support = not self.configuration.nfs_qcow2_volumes + if not self.multiattach_support: + msg = _("Multi-attach feature won't work " + "with qcow2 volumes enabled for nfs") + LOG.warning(msg) + + def _check_package_is_installed(self, package): + try: + self._execute(package, check_exit_code=False, + run_as_root=False) + except OSError as exc: + if exc.errno == errno.ENOENT: + msg = _('%s is not installed') % package + raise exception.VolumeDriverException(msg) + else: + raise + + def check_for_setup_error(self): + self._check_package_is_installed('dellfcopy') + + def _update_volume_stats(self): + super(PowerStoreNFSDriverInitialization, self)._update_volume_stats() + self._stats["vendor_name"] = self.VENDOR + self._stats['multiattach'] = self.multiattach_support + + +@interface.volumedriver +class PowerStoreNFSDriver(PowerStoreNFSDriverInitialization): + """Dell PowerStore NFS Driver. + + .. code-block:: none + + Version history: + 1.0.0 - Initial version + """ + + @coordination.synchronized('{self.driver_prefix}-{volume[id]}') + def delete_volume(self, volume): + """Deletes a logical volume.""" + + if not volume.provider_location: + LOG.warning("Volume %s does not have provider_location " + "specified, skipping", volume.name) + return + + self._ensure_share_mounted(volume.provider_location) + + info_path = self._local_path_volume_info(volume) + info = self._read_info_file(info_path, empty_if_missing=True) + + if info: + base_snap_path = os.path.join(self._local_volume_dir(volume), + info['active']) + self._delete(info_path) + self._delete(base_snap_path) + + base_volume_path = self._local_path_volume(volume) + self._delete(base_volume_path) + + def extend_volume(self, volume, new_size): + """Extend an existing volume to the new size.""" + + if self._is_volume_attached(volume): + msg = (_("Cannot extend volume %s while it is attached") + % volume.name_id) + raise exception.ExtendVolumeError(msg) + + LOG.info("Extending volume %s.", volume.name_id) + extend_by = int(new_size) - volume.size + if not self._is_share_eligible(volume.provider_location, + extend_by): + raise exception.ExtendVolumeError( + reason="Insufficient space to extend " + "volume %s to %sG" % (volume.name_id, new_size) + ) + + path = self.local_path(volume) + info = self._qemu_img_info(path, volume.name) + backing_fmt = info.file_format + + if backing_fmt not in ['raw', 'qcow2']: + msg = _("Unrecognized backing format: %s") % backing_fmt + raise exception.InvalidVolume(msg) + + image_utils.resize_image(path, new_size) + + def _do_fast_clone_file(self, volume_path, new_volume_path): + """Fast clone a file using a dellfcopy package.""" + + command = ['dellfcopy', '-o', 'fastclone', '-s', volume_path, + '-d', new_volume_path, '-v', '1'] + try: + LOG.info('Cloning file from %s to %s', + volume_path, new_volume_path) + self._execute(*command, run_as_root=self._execute_as_root) + LOG.info('Cloning volume: %s succeeded', volume_path) + except putils.ProcessExecutionError: + raise + + def _create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + + LOG.debug("Creating volume %(vol)s from snapshot %(snap)s", + {'vol': volume.name_id, 'snap': snapshot.id}) + + volume.provider_location = self._find_share(volume) + + self._copy_volume_from_snapshot(snapshot, volume, volume.size) + return {'provider_location': volume.provider_location} + + def _copy_volume_from_snapshot(self, snapshot, volume, volume_size, + src_encryption_key_id=None, + new_encryption_key_id=None): + """Copy snapshot to destination volume.""" + + LOG.debug("snapshot: %(snap)s, volume: %(vol)s, ", + {'snap': snapshot.id, + 'vol': volume.id, + 'size': volume_size}) + info_path = self._local_path_volume_info(snapshot.volume) + snap_info = self._read_info_file(info_path) + vol_path = self._local_volume_dir(snapshot.volume) + forward_file = snap_info[snapshot.id] + forward_path = os.path.join(vol_path, forward_file) + + img_info = self._qemu_img_info(forward_path, snapshot.volume.name) + path_to_snap_img = os.path.join(vol_path, img_info.backing_file) + + path_to_new_vol = self._local_path_volume(volume) + if img_info.backing_file_format == 'raw': + image_utils.convert_image(path_to_snap_img, + path_to_new_vol, + img_info.backing_file_format, + run_as_root=self._execute_as_root) + else: + self._do_fast_clone_file(path_to_snap_img, path_to_new_vol) + command = ['qemu-img', 'rebase', '-b', "", '-F', + img_info.backing_file_format, path_to_new_vol] + self._execute(*command, run_as_root=self._execute_as_root) + + self._set_rw_permissions(path_to_new_vol) + + def _create_cloned_volume(self, volume, src_vref, context): + """Clone src volume to destination volume.""" + + LOG.debug('Cloning volume %(src)s to volume %(dst)s', + {'src': src_vref.id, 'dst': volume.id}) + + volume_name = CONF.volume_name_template % volume.name_id + + vol_attrs = ['provider_location', 'size', 'id', 'name', 'status', + 'volume_type', 'metadata', 'obj_context'] + Volume = collections.namedtuple('Volume', vol_attrs) + volume_info = Volume(provider_location=src_vref.provider_location, + size=src_vref.size, + id=volume.name_id, + name=volume_name, + status=src_vref.status, + volume_type=src_vref.volume_type, + metadata=src_vref.metadata, + obj_context=volume.obj_context) + src_volue_path = self._local_path_volume(src_vref) + dst_volume = self._local_path_volume(volume_info) + self._do_fast_clone_file(src_volue_path, dst_volume) + + if src_vref.admin_metadata and 'format' in src_vref.admin_metadata: + volume.admin_metadata['format'] = ( + src_vref.admin_metadata['format']) + with volume.obj_as_admin(): + volume.save() + return {'provider_location': src_vref.provider_location} diff --git a/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-nfs.rst b/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-nfs.rst new file mode 100644 index 00000000000..e382319abc1 --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-nfs.rst @@ -0,0 +1,61 @@ +========================== +Dell PowerStore NFS Driver +========================== + +PowerStore NFS driver enables storing Block Storage service volumes on a +PowerStore storage back end. + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create, delete, attach and detach volumes. +- Create, delete volume snapshots. +- Create a volume from a snapshot. +- Copy an image to a volume. +- Copy a volume to an image. +- Clone a volume. +- Extend a volume. +- Get volume statistics. +- Attach a volume to multiple servers simultaneously (multiattach). +- Revert a volume to a snapshot. + +Driver configuration +~~~~~~~~~~~~~~~~~~~~ + +Add the following content into ``/etc/cinder/cinder.conf``: + +.. code-block:: ini + + [DEFAULT] + enabled_backends = powerstore-nfs + + [powerstore-nfs] + volume_driver = cinder.volume.drivers.dell_emc.powerstore.nfs.PowerStoreNFSDriver + nfs_qcow2_volumes = True + nfs_snapshot_support = True + nfs_sparsed_volumes = False + nas_host = + nas_share_path = /nfs-export + nas_secure_file_operations = False + nas_secure_file_permissions = False + volume_backend_name = powerstore-nfs + +Dell PowerStore NFS Copy Offload API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A feature for effective creation of a volume from snapshot/volume was added +in PowerStore NFS Driver. The dellfcopy utility provides the ability to copy +a file very quickly on a Dell SDNAS filesystem mounted by a client. +To download it, contact your local Dell representative. + +The dellfcopy tool is used in the following operations: + +- Create a volume from a snapshot. +- Clone a volume. + +To use PowerStore NFS driver with this feature, you must install the tool with +the following command: + +.. code-block:: console + + # sudo dpkg -i ./dellfcopy_1.3-1_amd64.deb diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index a379139b964..47e72ae5d8f 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -27,6 +27,9 @@ title=Dell PowerMax (2000, 8000) Storage Driver (iSCSI, FC) [driver.dell_emc_powerstore] title=Dell PowerStore Storage Driver (iSCSI, FC) +[driver.dell_emc_powerstore_nfs] +title=Dell PowerStore NFS Driver (NFS) + [driver.dell_emc_sc] title=Dell SC Series Storage Driver (iSCSI, FC) @@ -226,6 +229,7 @@ notes=A vendor driver is considered supported if the vendor is driver.datera=complete driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=complete driver.dell_emc_powervault=complete driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -298,6 +302,7 @@ notes=Cinder supports the ability to extend a volume that is attached to driver.datera=complete driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=complete driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -373,6 +378,7 @@ notes=Vendor drivers that support Quality of Service (QoS) at the driver.datera=complete driver.dell_emc_powermax=complete driver.dell_emc_powerstore=missing +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=missing driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -447,6 +453,7 @@ notes=Vendor drivers that support volume replication can report this driver.datera=missing driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=missing driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -522,6 +529,7 @@ notes=Vendor drivers that support consistency groups are able to driver.datera=missing driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=missing driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -596,6 +604,7 @@ notes=If a volume driver supports thin provisioning it means that it driver.datera=missing driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=complete driver.dell_emc_powervault=missing driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -671,6 +680,7 @@ notes=Storage assisted volume migration is like host assisted volume driver.datera=missing driver.dell_emc_powermax=complete driver.dell_emc_powerstore=missing +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=missing driver.dell_emc_sc=missing driver.dell_emc_unity=complete @@ -746,6 +756,7 @@ notes=Vendor drivers that report multi-attach support are able driver.datera=missing driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=complete driver.dell_emc_powervault=complete driver.dell_emc_sc=complete driver.dell_emc_unity=complete @@ -818,6 +829,7 @@ notes=Vendor drivers that implement the driver assisted function to revert a driver.datera=missing driver.dell_emc_powermax=complete driver.dell_emc_powerstore=complete +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=missing driver.dell_emc_sc=missing driver.dell_emc_unity=complete @@ -894,6 +906,7 @@ notes=Vendor drivers that support running in an active/active driver.datera=missing driver.dell_emc_powermax=missing driver.dell_emc_powerstore=missing +driver.dell_emc_powerstore_nfs=missing driver.dell_emc_powervault=missing driver.dell_emc_sc=missing driver.dell_emc_unity=missing diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 69f1aa7465c..b4dd206008f 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -137,4 +137,7 @@ ploop: CommandFilter, ploop, root mount.quobyte: CommandFilter, mount.quobyte, root umount.quobyte: CommandFilter, umount.quobyte, root +# cinder/volume/drivers/dell_emc/powerstore/nfs.py +dellfcopy: CommandFilter, dellfcopy, root + cryptsetup: CommandFilter, cryptsetup, root diff --git a/releasenotes/notes/bp-powerstore-nfs-cinder-driver-b743a8a89acafa35.yaml b/releasenotes/notes/bp-powerstore-nfs-cinder-driver-b743a8a89acafa35.yaml new file mode 100644 index 00000000000..f836fd10169 --- /dev/null +++ b/releasenotes/notes/bp-powerstore-nfs-cinder-driver-b743a8a89acafa35.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Dell PowerStore: Added NFS storage driver.