Add Cinder NFS driver for Dell PowerStore
Implements: blueprint powerstore-nfs-driver Co-Authored-By: Alexander Malashenko <alexander.malashenko@dell.com> Change-Id: Ide1d002acb8e1730767b15afc0566b2bb25999ed
This commit is contained in:
parent
db95ad2772
commit
a5ce771687
@ -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.
|
||||
|
462
cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_nfs.py
Normal file
462
cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_nfs.py
Normal file
@ -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)
|
238
cinder/volume/drivers/dell_emc/powerstore/nfs.py
Normal file
238
cinder/volume/drivers/dell_emc/powerstore/nfs.py
Normal file
@ -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}
|
@ -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 = <Ip>
|
||||
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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Dell PowerStore: Added NFS storage driver.
|
Loading…
Reference in New Issue
Block a user