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
This commit is contained in:
parent
735925d831
commit
c66572b432
@ -770,6 +770,20 @@ class GlusterfsNoSuitableShareFound(RemoteFSNoSuitableShareFound):
|
|||||||
message = _("There is no share which can host %(volume_size)sG")
|
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
|
# Fibre Channel Zone Manager
|
||||||
class ZoneManagerException(CinderException):
|
class ZoneManagerException(CinderException):
|
||||||
message = _("Fibre Channel connection control failure: %(reason)s")
|
message = _("Fibre Channel connection control failure: %(reason)s")
|
||||||
|
276
cinder/tests/unit/test_vzstorage.py
Normal file
276
cinder/tests/unit/test_vzstorage.py
Normal file
@ -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)
|
330
cinder/volume/drivers/vzstorage.py
Normal file
330
cinder/volume/drivers/vzstorage.py
Normal file
@ -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],...:/]<CLUSTER NAME>[: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)
|
@ -191,3 +191,7 @@ mv: CommandFilter, mv, root
|
|||||||
|
|
||||||
# cinder/volume/drivers/hgst.py
|
# cinder/volume/drivers/hgst.py
|
||||||
vgc-cluster: CommandFilter, vgc-cluster, root
|
vgc-cluster: CommandFilter, vgc-cluster, root
|
||||||
|
|
||||||
|
# cinder/volume/drivers/vzstorage.py
|
||||||
|
pstorage-mount: CommandFilter, pstorage-mount, root
|
||||||
|
pstorage: CommandFilter, pstorage, root
|
||||||
|
Loading…
Reference in New Issue
Block a user