From: Dmitry Guryanov Date: Thu, 18 Jun 2015 17:14:34 +0000 (+0300) Subject: Add Virtuozzo Storage Volume Driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=c66572b432c3dd6ac61f441bf7899ea7a333d98e;p=openstack-build%2Fcinder-build.git Add Virtuozzo Storage Volume Driver Add a volume driver which can use Virtuozzo Storage, which has filesystem interface and so volume driver has a similar workflow to NFS and SMBFS drivers. At this point the driver contain minimal set of features. Because I think some refactoring should be done in RemoteFS drivers before further development. For example code, which deals with image formats should go to the RemoteFS class. So I don't add qcow2 images support in this patch. Change-Id: If491c4220a77995d0c4247d152b49b0ff3fb0902 Partially-implements: blueprint virtuozzo-cloud-storage-support --- diff --git a/cinder/exception.py b/cinder/exception.py index 0f61f19f5..a8db1b040 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -770,6 +770,20 @@ class GlusterfsNoSuitableShareFound(RemoteFSNoSuitableShareFound): message = _("There is no share which can host %(volume_size)sG") +# Virtuozzo Storage Driver + +class VzStorageException(RemoteFSException): + message = _("Unknown Virtuozzo Storage exception") + + +class VzStorageNoSharesMounted(RemoteFSNoSharesMounted): + message = _("No mounted Virtuozzo Storage shares found") + + +class VzStorageNoSuitableShareFound(RemoteFSNoSuitableShareFound): + message = _("There is no share which can host %(volume_size)sG") + + # Fibre Channel Zone Manager class ZoneManagerException(CinderException): message = _("Fibre Channel connection control failure: %(reason)s") diff --git a/cinder/tests/unit/test_vzstorage.py b/cinder/tests/unit/test_vzstorage.py new file mode 100644 index 000000000..ab21d241d --- /dev/null +++ b/cinder/tests/unit/test_vzstorage.py @@ -0,0 +1,276 @@ +# Copyright 2015 Odin +# +# 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 copy +import errno +import os + +import mock + +from os_brick.remotefs import remotefs +from oslo_utils import units + +from cinder import exception +from cinder.image import image_utils +from cinder import test +from cinder.volume.drivers import vzstorage + + +class VZStorageTestCase(test.TestCase): + + _FAKE_SHARE = "10.0.0.1,10.0.0.2:/cluster123:123123" + _FAKE_MNT_BASE = '/mnt' + _FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, 'fake_hash') + _FAKE_VOLUME_NAME = 'volume-4f711859-4928-4cb7-801a-a50c37ceaccc' + _FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT, _FAKE_VOLUME_NAME) + _FAKE_VOLUME = {'id': '4f711859-4928-4cb7-801a-a50c37ceaccc', + 'size': 1, + 'provider_location': _FAKE_SHARE, + 'name': _FAKE_VOLUME_NAME, + 'status': 'available'} + _FAKE_SNAPSHOT_ID = '5g811859-4928-4cb7-801a-a50c37ceacba' + _FAKE_SNAPSHOT_PATH = ( + _FAKE_VOLUME_PATH + '-snapshot' + _FAKE_SNAPSHOT_ID) + _FAKE_SNAPSHOT = {'id': _FAKE_SNAPSHOT_ID, + 'volume': _FAKE_VOLUME, + 'status': 'available', + 'volume_size': 1} + + _FAKE_VZ_CONFIG = mock.MagicMock() + _FAKE_VZ_CONFIG.vzstorage_shares_config = '/fake/config/path' + _FAKE_VZ_CONFIG.vzstorage_sparsed_volumes = False + _FAKE_VZ_CONFIG.vzstorage_used_ratio = 0.7 + _FAKE_VZ_CONFIG.vzstorage_mount_point_base = _FAKE_MNT_BASE + _FAKE_VZ_CONFIG.nas_secure_file_operations = 'auto' + _FAKE_VZ_CONFIG.nas_secure_file_permissions = 'auto' + + def setUp(self): + super(VZStorageTestCase, self).setUp() + + self._remotefsclient = mock.patch.object(remotefs, + 'RemoteFsClient').start() + get_mount_point = mock.Mock(return_value=self._FAKE_MNT_POINT) + self._remotefsclient.get_mount_point = get_mount_point + cfg = copy.copy(self._FAKE_VZ_CONFIG) + self._vz_driver = vzstorage.VZStorageDriver(configuration=cfg) + self._vz_driver._local_volume_dir = mock.Mock( + return_value=self._FAKE_MNT_POINT) + self._vz_driver._execute = mock.Mock() + self._vz_driver.base = self._FAKE_MNT_BASE + + @mock.patch('os.path.exists') + def test_setup_ok(self, mock_exists): + mock_exists.return_value = True + self._vz_driver.do_setup(mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_missing_shares_conf(self, mock_exists): + mock_exists.return_value = False + self.assertRaises(exception.VzStorageException, + self._vz_driver.do_setup, + mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_invalid_usage_ratio(self, mock_exists): + mock_exists.return_value = True + self._vz_driver.configuration.vzstorage_used_ratio = 1.2 + self.assertRaises(exception.VzStorageException, + self._vz_driver.do_setup, + mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_invalid_usage_ratio2(self, mock_exists): + mock_exists.return_value = True + self._vz_driver.configuration.vzstorage_used_ratio = 0 + self.assertRaises(exception.VzStorageException, + self._vz_driver.do_setup, + mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_invalid_mount_point_base(self, mock_exists): + mock_exists.return_value = True + conf = copy.copy(self._FAKE_VZ_CONFIG) + conf.vzstorage_mount_point_base = './tmp' + vz_driver = vzstorage.VZStorageDriver(configuration=conf) + self.assertRaises(exception.VzStorageException, + vz_driver.do_setup, + mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_no_vzstorage(self, mock_exists): + mock_exists.return_value = True + exc = OSError() + exc.errno = errno.ENOENT + self._vz_driver._execute.side_effect = exc + self.assertRaises(exception.VzStorageException, + self._vz_driver.do_setup, + mock.sentinel.context) + + def test_initialize_connection(self): + drv = self._vz_driver + file_format = 'raw' + info = mock.Mock() + info.file_format = file_format + with mock.patch.object(drv, '_qemu_img_info', return_value=info): + ret = drv.initialize_connection(self._FAKE_VOLUME, None) + name = drv.get_active_image_from_info(self._FAKE_VOLUME) + expected = {'driver_volume_type': 'vzstorage', + 'data': {'export': self._FAKE_SHARE, + 'format': file_format, + 'name': name}, + 'mount_point_base': self._FAKE_MNT_BASE} + self.assertEqual(expected, ret) + + def test_ensure_share_mounted_invalid_share(self): + self.assertRaises(exception.VzStorageException, + self._vz_driver._ensure_share_mounted, ':') + + def test_ensure_share_mounted(self): + drv = self._vz_driver + share = self._FAKE_SHARE + drv.shares = {'1': '["1", "2", "3"]', share: '["some", "options"]'} + drv._ensure_share_mounted(share) + + def test_find_share(self): + drv = self._vz_driver + drv._mounted_shares = [self._FAKE_SHARE] + with mock.patch.object(drv, '_is_share_eligible', return_value=True): + ret = drv._find_share(1) + self.assertEqual(self._FAKE_SHARE, ret) + + def test_find_share_no_shares_mounted(self): + drv = self._vz_driver + with mock.patch.object(drv, '_is_share_eligible', return_value=True): + self.assertRaises(exception.VzStorageNoSharesMounted, + drv._find_share, 1) + + def test_find_share_no_shares_suitable(self): + drv = self._vz_driver + drv._mounted_shares = [self._FAKE_SHARE] + with mock.patch.object(drv, '_is_share_eligible', return_value=False): + self.assertRaises(exception.VzStorageNoSuitableShareFound, + drv._find_share, 1) + + def test_is_share_eligible_false(self): + drv = self._vz_driver + cap_info = (100 * units.Gi, 40 * units.Gi, 60 * units.Gi) + with mock.patch.object(drv, '_get_capacity_info', + return_value = cap_info): + ret = drv._is_share_eligible(self._FAKE_SHARE, 50) + self.assertEqual(False, ret) + + def test_is_share_eligible_true(self): + drv = self._vz_driver + cap_info = (100 * units.Gi, 40 * units.Gi, 60 * units.Gi) + with mock.patch.object(drv, '_get_capacity_info', + return_value = cap_info): + ret = drv._is_share_eligible(self._FAKE_SHARE, 30) + self.assertEqual(True, ret) + + @mock.patch.object(image_utils, 'resize_image') + def test_extend_volume(self, mock_resize_image): + drv = self._vz_driver + drv._check_extend_volume_support = mock.Mock(return_value=True) + drv._is_file_size_equal = mock.Mock(return_value=True) + + with mock.patch.object(drv, 'local_path', + return_value=self._FAKE_VOLUME_PATH): + drv.extend_volume(self._FAKE_VOLUME, 10) + + mock_resize_image.assert_called_once_with(self._FAKE_VOLUME_PATH, 10) + + def _test_check_extend_support(self, has_snapshots=False, + is_eligible=True): + drv = self._vz_driver + drv.local_path = mock.Mock(return_value=self._FAKE_VOLUME_PATH) + drv._is_share_eligible = mock.Mock(return_value=is_eligible) + + if has_snapshots: + active = self._FAKE_SNAPSHOT_PATH + else: + active = self._FAKE_VOLUME_PATH + + drv.get_active_image_from_info = mock.Mock(return_value=active) + if has_snapshots: + self.assertRaises(exception.InvalidVolume, + drv._check_extend_volume_support, + self._FAKE_VOLUME, 2) + elif not is_eligible: + self.assertRaises(exception.ExtendVolumeError, + drv._check_extend_volume_support, + self._FAKE_VOLUME, 2) + else: + drv._check_extend_volume_support(self._FAKE_VOLUME, 2) + drv._is_share_eligible.assert_called_once_with(self._FAKE_SHARE, 1) + + def test_check_extend_support(self): + self._test_check_extend_support() + + def test_check_extend_volume_with_snapshots(self): + self._test_check_extend_support(has_snapshots=True) + + def test_check_extend_volume_uneligible_share(self): + self._test_check_extend_support(is_eligible=False) + + @mock.patch.object(image_utils, 'convert_image') + def test_copy_volume_from_snapshot(self, mock_convert_image): + drv = self._vz_driver + + fake_volume_info = {self._FAKE_SNAPSHOT_ID: 'fake_snapshot_file_name'} + fake_img_info = mock.MagicMock() + fake_img_info.backing_file = self._FAKE_VOLUME_NAME + + drv.get_volume_format = mock.Mock(return_value='raw') + drv._local_path_volume_info = mock.Mock( + return_value=self._FAKE_VOLUME_PATH + '.info') + drv._local_volume_dir = mock.Mock( + return_value=self._FAKE_MNT_POINT) + drv._read_info_file = mock.Mock( + return_value=fake_volume_info) + drv._qemu_img_info = mock.Mock( + return_value=fake_img_info) + drv.local_path = mock.Mock( + return_value=self._FAKE_VOLUME_PATH[:-1]) + drv._extend_volume = mock.Mock() + + drv._copy_volume_from_snapshot( + self._FAKE_SNAPSHOT, self._FAKE_VOLUME, + self._FAKE_VOLUME['size']) + drv._extend_volume.assert_called_once_with( + self._FAKE_VOLUME, self._FAKE_VOLUME['size']) + mock_convert_image.assert_called_once_with( + self._FAKE_VOLUME_PATH, self._FAKE_VOLUME_PATH[:-1], 'raw') + + def test_delete_volume(self): + drv = self._vz_driver + fake_vol_info = self._FAKE_VOLUME_PATH + '.info' + + drv._ensure_share_mounted = mock.MagicMock() + fake_ensure_mounted = drv._ensure_share_mounted + + drv._local_volume_dir = mock.Mock( + return_value=self._FAKE_MNT_POINT) + drv.get_active_image_from_info = mock.Mock( + return_value=self._FAKE_VOLUME_NAME) + drv._delete = mock.Mock() + drv._local_path_volume_info = mock.Mock( + return_value=fake_vol_info) + + with mock.patch('os.path.exists', lambda x: True): + drv.delete_volume(self._FAKE_VOLUME) + + fake_ensure_mounted.assert_called_once_with(self._FAKE_SHARE) + drv._delete.assert_any_call( + self._FAKE_VOLUME_PATH) + drv._delete.assert_any_call(fake_vol_info) diff --git a/cinder/volume/drivers/vzstorage.py b/cinder/volume/drivers/vzstorage.py new file mode 100644 index 000000000..8f14d8b86 --- /dev/null +++ b/cinder/volume/drivers/vzstorage.py @@ -0,0 +1,330 @@ +# Copyright (c) 2015 Parallels IP Holdings GmbH +# 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 json +import os +import re + +from os_brick.remotefs import remotefs +from oslo_concurrency import processutils as putils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units + +from cinder import exception +from cinder.i18n import _, _LI +from cinder.image import image_utils +from cinder import utils +from cinder.volume.drivers import remotefs as remotefs_drv + +VERSION = '1.0' + +LOG = logging.getLogger(__name__) + +vzstorage_opts = [ + cfg.StrOpt('vzstorage_shares_config', + default='/etc/cinder/vzstorage_shares', + help='File with the list of available vzstorage shares.'), + cfg.BoolOpt('vzstorage_sparsed_volumes', + default=True, + help=('Create volumes as sparsed files which take no space ' + 'rather than regular files when using raw format, ' + 'in which case volume creation takes lot of time.')), + cfg.FloatOpt('vzstorage_used_ratio', + default=0.95, + help=('Percent of ACTUAL usage of the underlying volume ' + 'before no new volumes can be allocated to the volume ' + 'destination.')), + cfg.StrOpt('vzstorage_mount_point_base', + default='$state_path/mnt', + help=('Base dir containing mount points for ' + 'vzstorage shares.')), + cfg.ListOpt('vzstorage_mount_options', + default=None, + help=('Mount options passed to the vzstorage client. ' + 'See section of the pstorage-mount man page ' + 'for details.')), +] + +CONF = cfg.CONF +CONF.register_opts(vzstorage_opts) + + +class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): + """Cinder driver for Virtuozzo Storage. + + Creates volumes as files on the mounted vzstorage cluster. + + Version history: + 1.0 - Initial driver. + """ + driver_volume_type = 'vzstorage' + driver_prefix = 'vzstorage' + volume_backend_name = 'Virtuozzo_Storage' + VERSION = VERSION + SHARE_FORMAT_REGEX = r'(?:(\S+):\/)?([a-zA-Z0-9_-]+)(?::(\S+))?' + + def __init__(self, execute=putils.execute, *args, **kwargs): + self._remotefsclient = None + super(VZStorageDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(vzstorage_opts) + self._execute_as_root = False + root_helper = utils.get_root_helper() + # base bound to instance is used in RemoteFsConnector. + self.base = getattr(self.configuration, + 'vzstorage_mount_point_base', + CONF.vzstorage_mount_point_base) + opts = getattr(self.configuration, + 'vzstorage_mount_options', + CONF.vzstorage_mount_options) + + self._remotefsclient = remotefs.RemoteFsClient( + 'vzstorage', root_helper, execute=execute, + vzstorage_mount_point_base=self.base, + vzstorage_mount_options=opts) + + def _qemu_img_info(self, path, volume_name): + return super(VZStorageDriver, self)._qemu_img_info_base( + path, volume_name, self.configuration.vzstorage_mount_point_base) + + @remotefs_drv.locked_volume_id_operation + def initialize_connection(self, volume, connector): + """Allow connection to connector and return connection info. + + :param volume: volume reference + :param connector: connector reference + """ + # Find active image + active_file = self.get_active_image_from_info(volume) + active_file_path = os.path.join(self._local_volume_dir(volume), + active_file) + info = self._qemu_img_info(active_file_path, volume['name']) + fmt = info.file_format + + data = {'export': volume['provider_location'], + 'format': fmt, + 'name': active_file, + } + + return { + 'driver_volume_type': self.driver_volume_type, + 'data': data, + 'mount_point_base': self._get_mount_point_base(), + } + + def do_setup(self, context): + """Any initialization the volume driver does while starting.""" + super(VZStorageDriver, self).do_setup(context) + + config = self.configuration.vzstorage_shares_config + if not os.path.exists(config): + msg = (_("VzStorage config file at %(config)s doesn't exist.") % + {'config': config}) + LOG.error(msg) + raise exception.VzStorageException(msg) + + if not os.path.isabs(self.base): + msg = _("Invalid mount point base: %s.") % self.base + LOG.error(msg) + raise exception.VzStorageException(msg) + + used_ratio = self.configuration.vzstorage_used_ratio + if not ((used_ratio > 0) and (used_ratio <= 1)): + msg = _("VzStorage config 'vzstorage_used_ratio' invalid. " + "Must be > 0 and <= 1.0: %s.") % used_ratio + LOG.error(msg) + raise exception.VzStorageException(msg) + + self.shares = {} + + # Check if mount.fuse.pstorage is installed on this system; + # note that we don't need to be root to see if the package + # is installed. + package = 'mount.fuse.pstorage' + 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.VzStorageException(msg) + else: + raise + + self.configuration.nas_secure_file_operations = 'true' + self.configuration.nas_secure_file_permissions = 'true' + + def _ensure_share_mounted(self, share): + m = re.search(self.SHARE_FORMAT_REGEX, share) + if not m: + msg = (_("Invalid Virtuozzo Storage share specification: %r. " + "Must be: [MDS1[,MDS2],...:/][:PASSWORD].") + % share) + raise exception.VzStorageException(msg) + cluster_name = m.group(2) + + # set up logging to non-default path, so that it will + # be possible to mount the same cluster to another mount + # point by hand with default options. + mnt_flags = ['-l', '/var/log/pstorage/%s-cinder.log.gz' % cluster_name] + if self.shares.get(share) is not None: + extra_flags = json.loads(self.shares[share]) + mnt_flags.extend(extra_flags) + self._remotefsclient.mount(share, mnt_flags) + + def _find_share(self, volume_size_in_gib): + """Choose VzStorage share among available ones for given volume size. + + For instances with more than one share that meets the criteria, the + first suitable share will be selected. + + :param volume_size_in_gib: int size in GB + """ + + if not self._mounted_shares: + raise exception.VzStorageNoSharesMounted() + + for share in self._mounted_shares: + if self._is_share_eligible(share, volume_size_in_gib): + break + else: + raise exception.VzStorageNoSuitableShareFound( + volume_size=volume_size_in_gib) + + LOG.debug('Selected %s as target VzStorage share.', share) + + return share + + def _is_share_eligible(self, vz_share, volume_size_in_gib): + """Verifies VzStorage share is eligible to host volume with given size. + + :param vz_share: vzstorage share + :param volume_size_in_gib: int size in GB + """ + + used_ratio = self.configuration.vzstorage_used_ratio + volume_size = volume_size_in_gib * units.Gi + + total_size, available, allocated = self._get_capacity_info(vz_share) + + if (allocated + volume_size) / total_size > used_ratio: + LOG.debug('_is_share_eligible: %s is above ' + 'vzstorage_used_ratio.', vz_share) + return False + + return True + + @remotefs_drv.locked_volume_id_operation + def extend_volume(self, volume, size_gb): + LOG.info(_LI('Extending volume %s.'), volume['id']) + self._extend_volume(volume, size_gb) + + def _extend_volume(self, volume, size_gb): + volume_path = self.local_path(volume) + + self._check_extend_volume_support(volume, size_gb) + LOG.info(_LI('Resizing file to %sG...'), size_gb) + + self._do_extend_volume(volume_path, size_gb) + + def _do_extend_volume(self, volume_path, size_gb): + image_utils.resize_image(volume_path, size_gb) + + if not self._is_file_size_equal(volume_path, size_gb): + raise exception.ExtendVolumeError( + reason='Resizing image file failed.') + + def _check_extend_volume_support(self, volume, size_gb): + volume_path = self.local_path(volume) + active_file = self.get_active_image_from_info(volume) + active_file_path = os.path.join(self._local_volume_dir(volume), + active_file) + + if active_file_path != volume_path: + msg = _('Extend volume is only supported for this ' + 'driver when no snapshots exist.') + raise exception.InvalidVolume(msg) + + extend_by = int(size_gb) - 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['id'], size_gb)) + + def _is_file_size_equal(self, path, size): + """Checks if file size at path is equal to size.""" + data = image_utils.qemu_img_info(path) + virt_size = data.virtual_size / units.Gi + return virt_size == size + + def _copy_volume_from_snapshot(self, snapshot, volume, volume_size): + """Copy data from snapshot to destination volume. + + This is done with a qemu-img convert to raw/qcow2 from the snapshot + qcow2. + """ + + LOG.debug("_copy_volume_from_snapshot: snapshot: %(snap)s, " + "volume: %(vol)s, volume_size: %(size)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_dir = self._local_volume_dir(snapshot['volume']) + out_format = "raw" + + forward_file = snap_info[snapshot['id']] + forward_path = os.path.join(vol_dir, forward_file) + + # Find the file which backs this file, which represents the point + # when this snapshot was created. + img_info = self._qemu_img_info(forward_path, + snapshot['volume']['name']) + path_to_snap_img = os.path.join(vol_dir, img_info.backing_file) + + LOG.debug("_copy_volume_from_snapshot: will copy " + "from snapshot at %s.", path_to_snap_img) + + image_utils.convert_image(path_to_snap_img, + self.local_path(volume), + out_format) + self._extend_volume(volume, volume_size) + + @remotefs_drv.locked_volume_id_operation + def delete_volume(self, volume): + """Deletes a logical volume.""" + if not volume['provider_location']: + msg = (_('Volume %s does not have provider_location ' + 'specified, skipping.') % volume['name']) + LOG.error(msg) + raise exception.VzStorageException(msg) + + self._ensure_share_mounted(volume['provider_location']) + volume_dir = self._local_volume_dir(volume) + mounted_path = os.path.join(volume_dir, + self.get_active_image_from_info(volume)) + if os.path.exists(mounted_path): + self._delete(mounted_path) + else: + LOG.info(_LI("Skipping deletion of volume %s " + "as it does not exist."), mounted_path) + + info_path = self._local_path_volume_info(volume) + self._delete(info_path) diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 6d38a187d..79c2dd497 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -191,3 +191,7 @@ mv: CommandFilter, mv, root # cinder/volume/drivers/hgst.py vgc-cluster: CommandFilter, vgc-cluster, root + +# cinder/volume/drivers/vzstorage.py +pstorage-mount: CommandFilter, pstorage-mount, root +pstorage: CommandFilter, pstorage, root