diff --git a/cinder/opts.py b/cinder/opts.py index d01af19131e..91da0da7b84 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -149,6 +149,7 @@ from cinder.volume.drivers.veritas_access import veritas_iscsi as \ cinder_volume_drivers_veritas_access_veritasiscsi from cinder.volume.drivers.vmware import vmdk as \ cinder_volume_drivers_vmware_vmdk +from cinder.volume.drivers import vzstorage as cinder_volume_drivers_vzstorage from cinder.volume.drivers.windows import iscsi as \ cinder_volume_drivers_windows_iscsi from cinder.volume.drivers.windows import smbfs as \ @@ -348,6 +349,7 @@ def list_opts(): cinder_volume_drivers_stx_common.iscsi_opts, cinder_volume_drivers_synology_synologycommon.cinder_opts, cinder_volume_drivers_vmware_vmdk.vmdk_opts, + cinder_volume_drivers_vzstorage.vzstorage_opts, cinder_volume_drivers_windows_iscsi.windows_opts, cinder_volume_drivers_windows_smbfs.volume_opts, cinder_volume_drivers_zadara.zadara_opts, diff --git a/cinder/tests/unit/volume/drivers/test_vzstorage.py b/cinder/tests/unit/volume/drivers/test_vzstorage.py new file mode 100644 index 00000000000..38f9cef5c25 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/test_vzstorage.py @@ -0,0 +1,476 @@ +# 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 collections +import copy +import errno +import os +from unittest import mock + +import ddt +from os_brick.remotefs import remotefs +from oslo_utils import units + +from cinder import context +from cinder import exception +from cinder.image import image_utils +from cinder import test +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume +from cinder.volume.drivers import vzstorage + + +_orig_path_exists = os.path.exists + + +@ddt.ddt +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_SNAPSHOT_ID = '50811859-4928-4cb7-801a-a50c37ceacba' + _FAKE_SNAPSHOT_PATH = ( + _FAKE_VOLUME_PATH + '-snapshot' + _FAKE_SNAPSHOT_ID) + + def setUp(self): + super(VZStorageTestCase, self).setUp() + + self._cfg = mock.MagicMock() + self._cfg.vzstorage_shares_config = '/fake/config/path' + self._cfg.vzstorage_sparsed_volumes = False + self._cfg.vzstorage_used_ratio = 0.7 + self._cfg.vzstorage_mount_point_base = self._FAKE_MNT_BASE + self._cfg.vzstorage_default_volume_format = 'raw' + self._cfg.nas_secure_file_operations = 'auto' + self._cfg.nas_secure_file_permissions = 'auto' + + self._vz_driver = vzstorage.VZStorageDriver(configuration=self._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 + + self.context = context.get_admin_context() + vol_type = fake_volume.fake_volume_type_obj(self.context) + vol_type.extra_specs = {} + _FAKE_VOLUME = {'id': '4f711859-4928-4cb7-801a-a50c37ceaccc', + 'size': 1, + 'provider_location': self._FAKE_SHARE, + 'name': self._FAKE_VOLUME_NAME, + 'status': 'available'} + self.vol = fake_volume.fake_volume_obj(self.context, + volume_type_id=vol_type.id, + **_FAKE_VOLUME) + self.vol.volume_type = vol_type + + _FAKE_SNAPSHOT = {'id': self._FAKE_SNAPSHOT_ID, + 'status': 'available', + 'volume_size': 1} + self.snap = fake_snapshot.fake_snapshot_obj(self.context, + **_FAKE_SNAPSHOT) + self.snap.volume = self.vol + + def _path_exists(self, path): + if path.startswith(self._cfg.vzstorage_shares_config): + return True + return _orig_path_exists(path) + + def _path_dont_exists(self, path): + if path.startswith('/fake'): + return False + return _orig_path_exists(path) + + @mock.patch('os.path.exists') + def test_setup_ok(self, mock_exists): + mock_exists.side_effect = self._path_exists + self._vz_driver.do_setup(mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_missing_shares_conf(self, mock_exists): + mock_exists.side_effect = self._path_dont_exists + self.assertRaises(vzstorage.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.side_effect = self._path_exists + self._vz_driver.configuration.vzstorage_used_ratio = 1.2 + self.assertRaises(vzstorage.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.side_effect = self._path_exists + self._vz_driver.configuration.vzstorage_used_ratio = 0 + self.assertRaises(vzstorage.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.side_effect = self._path_exists + self._cfg.vzstorage_mount_point_base = './tmp' + vz_driver = vzstorage.VZStorageDriver(configuration=self._cfg) + self.assertRaises(vzstorage.VzStorageException, + vz_driver.do_setup, + mock.sentinel.context) + + @mock.patch('os.path.exists') + def test_setup_no_vzstorage(self, mock_exists): + mock_exists.side_effect = self._path_exists + exc = OSError() + exc.errno = errno.ENOENT + self._vz_driver._execute.side_effect = exc + self.assertRaises(vzstorage.VzStorageException, + self._vz_driver.do_setup, + mock.sentinel.context) + + @ddt.data({'qemu_fmt': 'parallels', 'glance_fmt': 'ploop'}, + {'qemu_fmt': 'qcow2', 'glance_fmt': 'qcow2'}) + @ddt.unpack + def test_initialize_connection(self, qemu_fmt, glance_fmt): + drv = self._vz_driver + info = mock.Mock() + info.file_format = qemu_fmt + snap_info = """{"volume_format": "%s", + "active": "%s"}""" % (qemu_fmt, self.vol.id) + with mock.patch.object(drv, '_qemu_img_info', return_value=info): + with mock.patch.object(drv, '_read_file', + return_value=snap_info): + ret = drv.initialize_connection(self.vol, None) + name = drv.get_active_image_from_info(self.vol) + expected = {'driver_volume_type': 'vzstorage', + 'data': {'export': self._FAKE_SHARE, + 'format': glance_fmt, + 'name': name}, + 'mount_point_base': self._FAKE_MNT_BASE} + self.assertEqual(expected, ret) + + def test_ensure_share_mounted_invalid_share(self): + self.assertRaises(vzstorage.VzStorageException, + self._vz_driver._ensure_share_mounted, ':') + + @mock.patch.object(remotefs.RemoteFsClient, 'mount') + def test_ensure_share_mounted(self, mock_mount): + drv = self._vz_driver + share = 'test' + expected_calls = [ + mock.call(share, ['-u', 'cinder', '-g', 'root', '-l', + '/var/log/vstorage/%s/cinder.log.gz' % share]), + mock.call(share, ['-l', '/var/log/dummy.log']) + ] + + share_flags = '["-u", "cinder", "-g", "root"]' + drv.shares[share] = share_flags + drv._ensure_share_mounted(share) + + share_flags = '["-l", "/var/log/dummy.log"]' + drv.shares[share] = share_flags + drv._ensure_share_mounted(share) + + mock_mount.assert_has_calls(expected_calls) + + 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(self.vol) + 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(vzstorage.VzStorageNoSharesMounted, + drv._find_share, self.vol) + + 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(vzstorage.VzStorageNoSuitableShareFound, + drv._find_share, self.vol) + + 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.assertFalse(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.assertTrue(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) + + snap_info = '{"active": "%s"}' % self.vol.id + with mock.patch.object(drv, 'get_volume_format', + return_value="raw"): + with mock.patch.object(drv, 'get_active_image_from_info', + return_value=self._FAKE_VOLUME_PATH): + with mock.patch.object(drv, '_read_file', + return_value=snap_info): + drv.extend_volume(self.vol, 10) + + mock_resize_image.assert_called_once_with(self._FAKE_VOLUME_PATH, 10) + + def _test_check_extend_support(self, 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) + + active = self._FAKE_VOLUME_PATH + + drv.get_active_image_from_info = mock.Mock(return_value=active) + if not is_eligible: + self.assertRaises(exception.ExtendVolumeError, + drv._check_extend_volume_support, + self.vol, 2) + else: + drv._check_extend_volume_support(self.vol, 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_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', + 'backing-files': + {self._FAKE_SNAPSHOT_ID: + self._FAKE_VOLUME_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.snap, self.vol, + self.vol['size']) + drv._extend_volume.assert_called_once_with( + self.vol, self.vol['size'], 'raw') + 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.vol) + + 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) + + @mock.patch('cinder.volume.drivers.remotefs.RemoteFSSnapDriverBase.' + '_write_info_file') + def test_delete_snapshot_ploop(self, _mock_write_info_file): + fake_snap_info = { + 'active': self._FAKE_VOLUME_NAME, + self._FAKE_SNAPSHOT_ID: self._FAKE_SNAPSHOT_PATH, + } + self._vz_driver.get_volume_format = mock.Mock( + return_value=vzstorage.DISK_FORMAT_PLOOP) + self._vz_driver._read_info_file = mock.Mock( + return_value=fake_snap_info + ) + self._vz_driver._get_desc_path = mock.Mock( + return_value='%s/DiskDescriptor.xml' % self._FAKE_VOLUME_PATH + ) + self._vz_driver.delete_snapshot(self.snap) + self._vz_driver._execute.assert_called_once_with( + 'ploop', 'snapshot-delete', '-u', + '{%s}' % self._FAKE_SNAPSHOT_ID, + '%s/DiskDescriptor.xml' % self._FAKE_VOLUME_PATH, + run_as_root=True + ) + + @mock.patch('cinder.volume.drivers.remotefs.RemoteFSSnapDriverBase.' + '_delete_snapshot') + def test_delete_snapshot_qcow2_invalid_snap_info(self, + mock_delete_snapshot): + fake_snap_info = { + 'active': self._FAKE_VOLUME_NAME, + } + self._vz_driver.get_volume_format = mock.Mock( + return_value=vzstorage.DISK_FORMAT_QCOW2) + self._vz_driver._read_info_file = mock.Mock( + return_value=fake_snap_info + ) + self._vz_driver.delete_snapshot(self.snap) + self.assertFalse(mock_delete_snapshot.called) + + def test_extend_volume_ploop(self): + drv = self._vz_driver + drv.get_active_image_from_info = mock.Mock( + return_value=self._FAKE_VOLUME_PATH) + drv.get_volume_format = mock.Mock( + return_value=vzstorage.DISK_FORMAT_PLOOP) + drv._is_share_eligible = mock.Mock( + return_value=True) + drv.extend_volume(self.vol, 100) + drv._execute.assert_called_once_with( + 'ploop', 'resize', '-s', '100G', + '%s/DiskDescriptor.xml' % self._FAKE_VOLUME_PATH, + run_as_root=True) + + @mock.patch.object(os.path, 'exists', return_value=False) + def test_do_create_volume_with_volume_type(self, mock_exists): + drv = self._vz_driver + drv.local_path = mock.Mock( + return_value=self._FAKE_VOLUME_PATH) + drv._write_info_file = mock.Mock() + drv._qemu_img_info = mock.Mock() + drv._create_qcow2_file = mock.Mock() + drv._create_ploop = mock.Mock() + + volume_type = fake_volume.fake_volume_type_obj(self.context) + volume_type.extra_specs = { + 'vz:volume_format': 'qcow2' + } + volume1 = fake_volume.fake_volume_obj(self.context) + volume1.size = 1024 + volume1.volume_type = volume_type + volume2 = copy.deepcopy(volume1) + volume2.metadata = { + 'volume_format': 'ploop' + } + + drv._do_create_volume(volume1) + drv._create_qcow2_file.assert_called_once_with( + self._FAKE_VOLUME_PATH, 1024) + + drv._do_create_volume(volume2) + drv._create_ploop.assert_called_once_with( + self._FAKE_VOLUME_PATH, 1024) + + @mock.patch('cinder.volume.drivers.remotefs.RemoteFSSnapDriver.' + '_create_cloned_volume') + @mock.patch.object(vzstorage.VZStorageDriver, 'get_volume_format', + return_value='qcow2') + def test_create_cloned_volume_qcow2(self, + mock_get_volume_format, + mock_remotefs_create_cloned_volume, + ): + drv = self._vz_driver + volume = fake_volume.fake_volume_obj(self.context) + src_vref_id = '375e32b2-804a-49f2-b282-85d1d5a5b9e1' + src_vref = fake_volume.fake_volume_obj( + self.context, + id=src_vref_id, + name='volume-%s' % src_vref_id, + provider_location=self._FAKE_SHARE) + src_vref.context = self.context + + mock_remotefs_create_cloned_volume.return_value = { + 'provider_location': self._FAKE_SHARE} + ret = drv.create_cloned_volume(volume, src_vref) + mock_remotefs_create_cloned_volume.assert_called_once_with( + volume, src_vref) + self.assertEqual(ret, {'provider_location': self._FAKE_SHARE}) + + @mock.patch.object(vzstorage.VZStorageDriver, '_local_path_volume_info') + @mock.patch.object(vzstorage.VZStorageDriver, '_create_snapshot_ploop') + @mock.patch.object(vzstorage.VZStorageDriver, 'delete_snapshot') + @mock.patch.object(vzstorage.VZStorageDriver, '_write_info_file') + @mock.patch.object(vzstorage.VZStorageDriver, '_copy_volume_from_snapshot') + @mock.patch.object(vzstorage.VZStorageDriver, 'get_volume_format', + return_value='ploop') + def test_create_cloned_volume_ploop(self, + mock_get_volume_format, + mock_copy_volume_from_snapshot, + mock_write_info_file, + mock_delete_snapshot, + mock_create_snapshot_ploop, + mock_local_path_volume_info, + ): + drv = self._vz_driver + volume = fake_volume.fake_volume_obj(self.context) + src_vref_id = '375e32b2-804a-49f2-b282-85d1d5a5b9e1' + src_vref = fake_volume.fake_volume_obj( + self.context, + id=src_vref_id, + name='volume-%s' % src_vref_id, + provider_location=self._FAKE_SHARE) + src_vref.context = self.context + + snap_attrs = ['volume_name', 'size', 'volume_size', 'name', + 'volume_id', 'id', 'volume'] + Snapshot = collections.namedtuple('Snapshot', snap_attrs) + + snap_ref = Snapshot(volume_name=volume.name, + name='clone-snap-%s' % src_vref.id, + size=src_vref.size, + volume_size=src_vref.size, + volume_id=src_vref.id, + id=src_vref.id, + volume=src_vref) + + def _check_provider_location(volume): + self.assertEqual(volume.provider_location, self._FAKE_SHARE) + return mock.sentinel.fake_info_path + mock_local_path_volume_info.side_effect = _check_provider_location + + ret = drv.create_cloned_volume(volume, src_vref) + self.assertEqual(ret, {'provider_location': self._FAKE_SHARE}) + + mock_write_info_file.assert_called_once_with( + mock.sentinel.fake_info_path, {'active': 'volume-%s' % volume.id}) + mock_create_snapshot_ploop.assert_called_once_with(snap_ref) + mock_copy_volume_from_snapshot.assert_called_once_with( + snap_ref, volume, volume.size) + mock_delete_snapshot.assert_called_once_with(snap_ref) diff --git a/cinder/volume/drivers/vzstorage.py b/cinder/volume/drivers/vzstorage.py new file mode 100644 index 00000000000..531b69bf3fc --- /dev/null +++ b/cinder/volume/drivers/vzstorage.py @@ -0,0 +1,743 @@ +# 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 collections +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 imageutils +from oslo_utils import units + +from cinder import exception +from cinder.i18n import _ +from cinder.image import image_utils +from cinder import interface +from cinder import utils +from cinder.volume import configuration +from cinder.volume.drivers import remotefs as remotefs_drv + +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', + help=('Mount options passed to the vzstorage client. ' + 'See section of the pstorage-mount man page ' + 'for details.')), + cfg.StrOpt('vzstorage_default_volume_format', + default='raw', + help=('Default format that will be used when creating volumes ' + 'if no volume format is specified.')), +] + +CONF = cfg.CONF +CONF.register_opts(vzstorage_opts, group=configuration.SHARED_CONF_GROUP) + +PLOOP_BASE_DELTA_NAME = 'root.hds' +DISK_FORMAT_RAW = 'raw' +DISK_FORMAT_QCOW2 = 'qcow2' +DISK_FORMAT_PLOOP = 'ploop' + + +class VzStorageException(exception.RemoteFSException): + message = _("Unknown Virtuozzo Storage exception") + + +class VzStorageNoSharesMounted(exception.RemoteFSNoSharesMounted): + message = _("No mounted Virtuozzo Storage shares found") + + +class VzStorageNoSuitableShareFound(exception.RemoteFSNoSuitableShareFound): + message = _("There is no share which can host %(volume_size)sG") + + +class PloopDevice(object): + """Setup a ploop device for ploop image + + This class is for mounting ploop devices using with statement: + with PloopDevice('/vzt/private/my-ct/harddisk.hdd') as dev_path: + # do something + + :param path: A path to ploop harddisk dir + :param snapshot_id: Snapshot id to mount + :param execute: execute helper + """ + + def __init__(self, path, snapshot_id=None, read_only=True, + execute=putils.execute): + self.path = path + self.snapshot_id = snapshot_id + self.read_only = read_only + self.execute = execute + + def __enter__(self): + self.dd_path = os.path.join(self.path, 'DiskDescriptor.xml') + cmd = ['ploop', 'mount', self.dd_path] + + if self.snapshot_id: + cmd.append('-u') + cmd.append(self.snapshot_id) + + if self.read_only: + cmd.append('-r') + + out, err = self.execute(*cmd, run_as_root=True) + + m = re.search(r'dev=(\S+)', out) + if not m: + raise Exception('Invalid output from ploop mount: %s' % out) + + self.ploop_dev = m.group(1) + + return self.ploop_dev + + def _umount(self): + self.execute('ploop', 'umount', self.dd_path, run_as_root=True) + + def __exit__(self, type, value, traceback): + self._umount() + + +@interface.volumedriver +class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): + """Cinder driver for Virtuozzo Storage. + + Creates volumes as files on the mounted vzstorage cluster. + + .. code-block:: none + + Version history: + 1.0 - Initial driver. + 1.1 - Supports vz:volume_format in vendor properties. + """ + VERSION = '1.1' + CI_WIKI_NAME = "Virtuozzo_Storage_CI" + + # TODO(jsbryant) Remove driver in the 'U' release if CI is not fixed. + SUPPORTED = False + + SHARE_FORMAT_REGEX = r'(?:(\S+):\/)?([a-zA-Z0-9_-]+)(?::(\S+))?' + + def __init__(self, execute=putils.execute, *args, **kwargs): + self.driver_volume_type = 'vzstorage' + self.driver_prefix = 'vzstorage' + self.volume_backend_name = 'Virtuozzo_Storage' + 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 = self.configuration.vzstorage_mount_point_base + opts = self.configuration.vzstorage_mount_options + + self._remotefsclient = remotefs.VZStorageRemoteFSClient( + 'vzstorage', root_helper, execute=execute, + vzstorage_mount_point_base=self.base, + vzstorage_mount_options=opts) + + @staticmethod + def get_driver_options(): + return vzstorage_opts + + def _update_volume_stats(self): + super(VZStorageDriver, self)._update_volume_stats() + self._stats['vendor_name'] = 'Virtuozzo' + + def _init_vendor_properties(self): + namespace = 'vz' + properties = {} + + self._set_property( + properties, + "%s:volume_format" % namespace, + "Volume format", + _("Specifies volume format."), + "string", + enum=["qcow2", "ploop", "raw"], + default=self.configuration.vzstorage_default_volume_format) + + return properties, namespace + + def _qemu_img_info(self, path, volume_name): + qemu_img_cache = path + ".qemu_img_info" + is_cache_outdated = True + if os.path.isdir(path): + # Ploop disks stored along with metadata xml as directories + # qemu-img should explore base data file inside + path = os.path.join(path, PLOOP_BASE_DELTA_NAME) + if os.path.isfile(qemu_img_cache): + info_tm = os.stat(qemu_img_cache).st_mtime + snap_tm = os.stat(path).st_mtime + if info_tm >= snap_tm: + is_cache_outdated = False + if is_cache_outdated: + LOG.debug("Cached qemu-img info %s not present or outdated," + " refresh", qemu_img_cache) + ret = super(VZStorageDriver, self)._qemu_img_info_base( + path, volume_name, + self.configuration.vzstorage_mount_point_base) + # We need only backing_file and file_format + d = {'file_format': ret.file_format, + 'backing_file': ret.backing_file} + with open(qemu_img_cache, "w") as f: + json.dump(d, f) + else: + ret = imageutils.QemuImgInfo() + with open(qemu_img_cache, "r") as f: + cached_data = json.load(f) + ret.file_format = cached_data['file_format'] + ret.backing_file = cached_data['backing_file'] + return ret + + @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) + + data = {'export': volume.provider_location, + 'format': self.get_volume_format(volume), + '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 VzStorageException(msg) + + if not os.path.isabs(self.base): + msg = _("Invalid mount point base: %s.") % self.base + LOG.error(msg) + raise 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 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 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 VzStorageException(msg) + cluster_name = m.group(2) + + if share in self.shares: + mnt_flags = json.loads(self.shares[share]) + else: + mnt_flags = [] + + if '-l' not in mnt_flags: + # If logging path is not specified in shares config + # 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.extend([ + '-l', '/var/log/vstorage/%s/cinder.log.gz' % cluster_name]) + + self._remotefsclient.mount(share, mnt_flags) + + def _find_share(self, volume): + """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: the volume to be created. + """ + + if not self._mounted_shares: + raise VzStorageNoSharesMounted() + + for share in self._mounted_shares: + if self._is_share_eligible(share, volume.size): + break + else: + raise VzStorageNoSuitableShareFound( + volume_size=volume.size) + + 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 + + def choose_volume_format(self, volume): + volume_format = None + volume_type = volume.volume_type + + # Retrieve volume format from volume metadata + if 'volume_format' in volume.metadata: + volume_format = volume.metadata['volume_format'] + + # If volume format wasn't found in metadata, use + # volume type extra specs + if not volume_format and volume_type: + extra_specs = volume_type.extra_specs or {} + if 'vz:volume_format' in extra_specs: + volume_format = extra_specs['vz:volume_format'] + + # If volume format is still undefined, return default + # volume format from backend configuration + return (volume_format or + self.configuration.vzstorage_default_volume_format) + + def get_volume_format(self, volume): + active_file = self.get_active_image_from_info(volume) + active_file_path = os.path.join(self._local_volume_dir(volume), + active_file) + img_info = self._qemu_img_info(active_file_path, volume.name) + return image_utils.from_qemu_img_disk_format(img_info.file_format) + + def _create_ploop(self, volume_path, volume_size): + os.mkdir(volume_path) + try: + self._execute('ploop', 'init', '-s', '%sG' % volume_size, + os.path.join(volume_path, PLOOP_BASE_DELTA_NAME), + run_as_root=True) + except putils.ProcessExecutionError: + os.rmdir(volume_path) + raise + + def _do_create_volume(self, volume): + """Create a volume on given vzstorage share. + + :param volume: volume reference + """ + volume_format = self.choose_volume_format(volume) + volume_path = self.local_path(volume) + volume_size = volume.size + + LOG.debug("Creating new volume at %s.", volume_path) + + if os.path.exists(volume_path): + msg = _('File already exists at %s.') % volume_path + LOG.error(msg) + raise exception.InvalidVolume(reason=msg) + + if volume_format == DISK_FORMAT_PLOOP: + self._create_ploop(volume_path, volume_size) + elif volume_format == DISK_FORMAT_QCOW2: + self._create_qcow2_file(volume_path, volume_size) + elif self.configuration.vzstorage_sparsed_volumes: + self._create_sparsed_file(volume_path, volume_size) + else: + self._create_regular_file(volume_path, volume_size) + + info_path = self._local_path_volume_info(volume) + snap_info = {'active': os.path.basename(volume_path)} + self._write_info_file(info_path, snap_info) + + # Query qemu-img info to cache the output + self._qemu_img_info(volume_path, volume.name) + + def _delete(self, path): + self._execute('rm', '-rf', path, run_as_root=True) + + @remotefs_drv.locked_volume_id_operation + def extend_volume(self, volume, size_gb): + LOG.info('Extending volume %s.', volume.id) + volume_format = self.get_volume_format(volume) + self._extend_volume(volume, size_gb, volume_format) + + def _extend_volume(self, volume, size_gb, volume_format): + self._check_extend_volume_support(volume, size_gb) + LOG.info('Resizing file to %sG...', size_gb) + + active_path = os.path.join( + self._get_mount_point_for_share(volume.provider_location), + self.get_active_image_from_info(volume)) + self._do_extend_volume(active_path, size_gb, volume_format) + + def _do_extend_volume(self, volume_path, size_gb, volume_format): + + if volume_format == DISK_FORMAT_PLOOP: + self._execute('ploop', 'resize', '-s', + '%dG' % size_gb, + os.path.join(volume_path, 'DiskDescriptor.xml'), + run_as_root=True) + else: + 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): + 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 _recreate_ploop_desc(self, image_dir, image_file): + self._delete(os.path.join(image_dir, 'DiskDescriptor.xml')) + + self._execute('ploop', 'restore-descriptor', image_dir, image_file) + + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + volume_format = self.get_volume_format(volume) + qemu_volume_format = image_utils.fixup_disk_format(volume_format) + image_path = self.local_path(volume) + if volume_format == DISK_FORMAT_PLOOP: + image_path = os.path.join(image_path, PLOOP_BASE_DELTA_NAME) + + image_utils.fetch_to_volume_format( + context, image_service, image_id, + image_path, qemu_volume_format, + self.configuration.volume_dd_blocksize) + + if volume_format == DISK_FORMAT_PLOOP: + self._recreate_ploop_desc(self.local_path(volume), image_path) + + self._do_extend_volume(self.local_path(volume), + volume.size, + volume_format) + # Query qemu-img info to cache the output + self._qemu_img_info(self.local_path(volume), volume.name) + + 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. + """ + + 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 = self.choose_volume_format(volume) + qemu_out_format = image_utils.fixup_disk_format(out_format) + volume_format = self.get_volume_format(snapshot.volume) + volume_path = self.local_path(volume) + + if volume_format in (DISK_FORMAT_QCOW2, DISK_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, + volume_path, + qemu_out_format) + elif volume_format == DISK_FORMAT_PLOOP: + with PloopDevice(self.local_path(snapshot.volume), + snapshot.id, + execute=self._execute) as dev: + base_file = os.path.join(volume_path, 'root.hds') + image_utils.convert_image(dev, + base_file, + qemu_out_format) + else: + msg = _("Unsupported volume format %s") % volume_format + raise exception.InvalidVolume(msg) + + self._extend_volume(volume, volume_size, out_format) + # Query qemu-img info to cache the output + img_info = self._qemu_img_info(volume_path, volume.name) + + @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) + return + + 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) + self._delete(mounted_path + ".qemu_img_info") + else: + LOG.info("Skipping deletion of volume %s " + "as it does not exist.", mounted_path) + + info_path = self._local_path_volume_info(volume) + self._delete(info_path) + + def _get_desc_path(self, volume): + return os.path.join(self.local_path(volume), 'DiskDescriptor.xml') + + def _create_snapshot_ploop(self, snapshot): + status = snapshot.volume.status + if status != 'available': + msg = (_('Volume status must be available for ' + 'snapshot %(id)s. (is %(status)s)') % + {'id': snapshot.id, 'status': status}) + raise exception.InvalidVolume(msg) + + info_path = self._local_path_volume_info(snapshot.volume) + snap_info = self._read_info_file(info_path) + self._execute('ploop', 'snapshot', '-u', '{%s}' % snapshot.id, + self._get_desc_path(snapshot.volume), + run_as_root=True) + snap_file = os.path.join('volume-%s' % snapshot.volume.id, snapshot.id) + snap_info[snapshot.id] = snap_file + self._write_info_file(info_path, snap_info) + + def _delete_snapshot_ploop(self, snapshot): + status = snapshot.volume.status + if status != 'available': + msg = (_('Volume status must be available for ' + 'snapshot %(id)s. (is %(status)s)') % + {'id': snapshot.id, 'status': status}) + raise exception.InvalidVolume(msg) + + info_path = self._local_path_volume_info(snapshot.volume) + snap_info = self._read_info_file(info_path) + self._execute('ploop', 'snapshot-delete', '-u', '{%s}' % snapshot.id, + self._get_desc_path(snapshot.volume), + run_as_root=True) + snap_info.pop(snapshot.id, None) + self._write_info_file(info_path, snap_info) + + def _create_snapshot(self, snapshot): + volume_format = self.get_volume_format(snapshot.volume) + if volume_format == DISK_FORMAT_PLOOP: + self._create_snapshot_ploop(snapshot) + else: + super(VZStorageDriver, self)._create_snapshot(snapshot) + + def _do_create_snapshot(self, snapshot, backing_filename, + new_snap_path): + super(VZStorageDriver, self)._do_create_snapshot(snapshot, + backing_filename, + new_snap_path) + # Cache qemu-img info for created snapshot + self._qemu_img_info(new_snap_path, snapshot.volume.name) + + def _delete_snapshot_qcow2(self, snapshot): + info_path = self._local_path_volume_info(snapshot.volume) + snap_info = self._read_info_file(info_path, empty_if_missing=True) + if snapshot.id not in snap_info: + LOG.warning("Snapshot %s doesn't exist in snap_info", + snapshot.id) + return + + snap_file = os.path.join(self._local_volume_dir(snapshot.volume), + snap_info[snapshot.id]) + active_file = os.path.join(self._local_volume_dir(snapshot.volume), + snap_info['active']) + higher_file = self._get_higher_image_path(snapshot) + if higher_file: + higher_file = os.path.join(self._local_volume_dir(snapshot.volume), + higher_file) + elif active_file != snap_file: + msg = (_("Expected higher file exists for snapshot %s") % + snapshot.id) + raise VzStorageException(msg) + + img_info = self._qemu_img_info(snap_file, snapshot.volume.name) + base_file = os.path.join(self._local_volume_dir(snapshot.volume), + img_info.backing_file) + + super(VZStorageDriver, self)._delete_snapshot(snapshot) + + def _qemu_info_cache(fn): + return fn + ".qemu_img_info" + + def _update_backing_file(info_src, info_dst): + with open(info_src, 'r') as fs, open(info_dst, 'r') as fd: + src = json.load(fs) + dst = json.load(fd) + dst['backing_file'] = src['backing_file'] + with open(info_dst, 'w') as fdw: + json.dump(dst, fdw) + + if snap_file != active_file: + # mv snap_file.info higher_file.info + _update_backing_file( + _qemu_info_cache(snap_file), + _qemu_info_cache(higher_file)) + self._delete(_qemu_info_cache(snap_file)) + elif snapshot.volume.status == 'in-use': + # mv base_file.info snap_file.info + _update_backing_file( + _qemu_info_cache(base_file), + _qemu_info_cache(snap_file)) + self._delete(_qemu_info_cache(base_file)) + else: + # rm snap_file.info + self._delete(_qemu_info_cache(snap_file)) + + def _delete_snapshot(self, snapshot): + volume_format = self.get_volume_format(snapshot.volume) + if volume_format == DISK_FORMAT_PLOOP: + self._delete_snapshot_ploop(snapshot) + else: + self._delete_snapshot_qcow2(snapshot) + + def _copy_volume_to_image(self, context, volume, image_service, + image_meta): + """Copy the volume to the specified image.""" + + volume_format = self.get_volume_format(volume) + if volume_format == DISK_FORMAT_PLOOP: + with PloopDevice(self.local_path(volume), + execute=self._execute) as dev: + image_utils.upload_volume(context, + image_service, + image_meta, + dev, + volume_format='raw') + else: + super(VZStorageDriver, self)._copy_volume_to_image(context, volume, + image_service, + image_meta) + + def _create_cloned_volume_ploop(self, volume, src_vref): + LOG.info('Cloning volume %(src)s to volume %(dst)s', + {'src': src_vref.id, + 'dst': volume.id}) + + if src_vref.status != 'available': + msg = _("Volume status must be 'available'.") + raise exception.InvalidVolume(msg) + + volume_name = CONF.volume_name_template % volume.id + + # Create fake snapshot object + snap_attrs = ['volume_name', 'size', 'volume_size', 'name', + 'volume_id', 'id', 'volume'] + Snapshot = collections.namedtuple('Snapshot', snap_attrs) + + temp_snapshot = Snapshot(id=src_vref.id, + volume_name=volume_name, + size=src_vref.size, + volume_size=src_vref.size, + name='clone-snap-%s' % src_vref.id, + volume_id=src_vref.id, + volume=src_vref) + + self._create_snapshot_ploop(temp_snapshot) + try: + volume.provider_location = src_vref.provider_location + info_path = self._local_path_volume_info(volume) + snap_info = {'active': 'volume-%s' % volume.id} + self._write_info_file(info_path, snap_info) + self._copy_volume_from_snapshot(temp_snapshot, + volume, + volume.size) + + finally: + self.delete_snapshot(temp_snapshot) + + return {'provider_location': src_vref.provider_location} + + def _create_cloned_volume(self, volume, src_vref, context): + """Creates a clone of the specified volume.""" + volume_format = self.get_volume_format(src_vref) + if volume_format == DISK_FORMAT_PLOOP: + return self._create_cloned_volume_ploop(volume, src_vref) + else: + return super(VZStorageDriver, self)._create_cloned_volume(volume, + src_vref) diff --git a/doc/source/configuration/block-storage/drivers/vzstorage-driver.rst b/doc/source/configuration/block-storage/drivers/vzstorage-driver.rst new file mode 100644 index 00000000000..c0ed8ab5519 --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/vzstorage-driver.rst @@ -0,0 +1,17 @@ +======================== +Virtuozzo Storage driver +======================== + +The Virtuozzo Storage driver is a fault-tolerant distributed storage +system that is optimized for virtualization workloads. +Set the following in your ``cinder.conf`` file, and use the following +options to configure it. + +.. code-block:: ini + + volume_driver = cinder.volume.drivers.vzstorage.VZStorageDriver + +.. config-table:: + :config-target: Virtuozzo Storage + + cinder.volume.drivers.vzstorage diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 8daaa3578f4..100515156b4 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -165,6 +165,9 @@ title=Veritas Access iSCSI Driver (iSCSI) [driver.vrtscnfs] title=Veritas Cluster NFS Driver (NFS) +[driver.vzstorage] +title=Virtuozzo Storage Driver (remotefs) + [driver.vmware] title=VMware Storage Driver (vmdk) @@ -237,6 +240,7 @@ driver.storpool=complete driver.synology=complete driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=complete driver.win_iscsi=complete driver.win_smb=complete @@ -297,6 +301,7 @@ driver.storpool=complete driver.synology=complete driver.vrtsaccess=complete driver.vrtscnfs=complete +driver.vzstorage=complete driver.vmware=complete driver.win_iscsi=complete driver.win_smb=complete @@ -357,6 +362,7 @@ driver.storpool=missing driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=complete @@ -420,6 +426,7 @@ driver.storpool=missing driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=missing @@ -482,6 +489,7 @@ driver.storpool=complete driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=missing @@ -545,6 +553,7 @@ driver.storpool=missing driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=missing @@ -607,6 +616,7 @@ driver.storpool=complete driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=complete @@ -670,6 +680,7 @@ driver.storpool=complete driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=missing @@ -733,6 +744,7 @@ driver.storpool=complete driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=missing @@ -793,6 +805,7 @@ driver.storpool=missing driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=complete driver.win_iscsi=missing driver.win_smb=missing @@ -857,6 +870,7 @@ driver.storpool=missing driver.synology=missing driver.vrtsaccess=missing driver.vrtscnfs=missing +driver.vzstorage=missing driver.vmware=missing driver.win_iscsi=missing driver.win_smb=missing diff --git a/doc/source/reference/support-matrix.rst b/doc/source/reference/support-matrix.rst index 7af5fc456f4..0a2aec3b16f 100644 --- a/doc/source/reference/support-matrix.rst +++ b/doc/source/reference/support-matrix.rst @@ -90,4 +90,3 @@ release. * HPE Lefthand Driver (iSCSI) * ProphetStor Flexvisor Driver * Sheepdog Driver - * Virtuozzo Storage Driver diff --git a/releasenotes/notes/virtuozzo-storage-driver-removal-d53942afce6465d0.yaml b/releasenotes/notes/virtuozzo-storage-driver-removal-d53942afce6465d0.yaml deleted file mode 100644 index 7d06a3810e2..00000000000 --- a/releasenotes/notes/virtuozzo-storage-driver-removal-d53942afce6465d0.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -upgrade: - - | - The Virtuozzo Storage driver was marked unsupported in the - Train release and has now been removed. All data on - Virtuozzo Storage backends should be migrated to a supported - storage backend before upgrading your Cinder installation.