Remove Linux SMBFS driver

This driver has been deprecated in Ocata and is now being removed.

Note that the Windows SMBFS driver was inheriting it so we're pulling
in the common logic.

Change-Id: I3dd8ceb042223402bfc29050f74ad3672badf816
This commit is contained in:
Lucian Petrut 2017-02-15 16:27:15 +02:00
parent a45d5c7751
commit 137fd502a9
5 changed files with 890 additions and 1488 deletions

View File

@ -167,7 +167,6 @@ from cinder.volume.drivers.san.hp import hpmsa_common as \
cinder_volume_drivers_san_hp_hpmsacommon
from cinder.volume.drivers.san import san as cinder_volume_drivers_san_san
from cinder.volume.drivers import sheepdog as cinder_volume_drivers_sheepdog
from cinder.volume.drivers import smbfs as cinder_volume_drivers_smbfs
from cinder.volume.drivers import solidfire as cinder_volume_drivers_solidfire
from cinder.volume.drivers.synology import synology_common as \
cinder_volume_drivers_synology_synologycommon
@ -178,6 +177,8 @@ from cinder.volume.drivers.violin import v7000_common as \
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 smbfs as \
cinder_volume_drivers_windows_smbfs
from cinder.volume.drivers.windows import windows as \
cinder_volume_drivers_windows_windows
from cinder.volume.drivers import xio as cinder_volume_drivers_xio
@ -347,7 +348,6 @@ def list_opts():
cinder_volume_drivers_san_hp_hpmsacommon.iscsi_opts,
cinder_volume_drivers_san_san.san_opts,
cinder_volume_drivers_sheepdog.sheepdog_opts,
cinder_volume_drivers_smbfs.volume_opts,
cinder_volume_drivers_solidfire.sf_opts,
cinder_volume_drivers_synology_synologycommon.cinder_opts,
cinder_volume_drivers_tegile.tegile_opts,
@ -355,6 +355,7 @@ def list_opts():
cinder_volume_drivers_violin_v7000common.violin_opts,
cinder_volume_drivers_vmware_vmdk.vmdk_opts,
cinder_volume_drivers_vzstorage.vzstorage_opts,
cinder_volume_drivers_windows_smbfs.volume_opts,
cinder_volume_drivers_windows_windows.windows_opts,
cinder_volume_drivers_xio.XIO_OPTS,
cinder_volume_drivers_zadara.zadara_opts,

View File

@ -1,787 +0,0 @@
# Copyright 2014 Cloudbase Solutions Srl
#
# 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 functools
import os
import ddt
import mock
from oslo_utils import fileutils
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 remotefs
from cinder.volume.drivers import smbfs
def requires_allocation_data_update(expected_size):
def wrapper(func):
@functools.wraps(func)
def inner(inst, *args, **kwargs):
with mock.patch.object(
inst._smbfs_driver,
'update_disk_allocation_data') as fake_update:
func(inst, *args, **kwargs)
fake_update.assert_called_once_with(inst.volume,
expected_size)
return inner
return wrapper
@ddt.ddt
class SmbFsTestCase(test.TestCase):
_FAKE_SHARE = '//1.2.3.4/share1'
_FAKE_SHARE_HASH = 'db0bf952c1734092b83e8990bd321131'
_FAKE_MNT_BASE = '/mnt'
_FAKE_VOLUME_NAME = 'volume-4f711859-4928-4cb7-801a-a50c37ceaccc'
_FAKE_TOTAL_SIZE = '2048'
_FAKE_TOTAL_AVAILABLE = '1024'
_FAKE_TOTAL_ALLOCATED = 1024
_FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, _FAKE_SHARE_HASH)
_FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT, _FAKE_VOLUME_NAME)
_FAKE_VOLUME_SIZE = 1
_FAKE_SNAPSHOT_ID = '50811859-4928-4cb7-801a-a50c37ceacba'
_FAKE_SNAPSHOT_PATH = (
_FAKE_VOLUME_PATH + '-snapshot' + _FAKE_SNAPSHOT_ID)
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
_FAKE_OPTIONS_DICT = {'username': 'Administrator',
'password': '12345'}
_FAKE_ALLOCATION_DATA_PATH = os.path.join('fake_dir',
'fake_allocation_data')
def setUp(self):
super(SmbFsTestCase, self).setUp()
self._FAKE_SMBFS_CONFIG = mock.MagicMock(
smbfs_oversub_ratio = 2,
smbfs_used_ratio = 0.5,
smbfs_shares_config = '/fake/config/path',
smbfs_default_volume_format = 'raw',
smbfs_sparsed_volumes = False)
self._smbfs_driver = smbfs.SmbfsDriver(configuration=mock.Mock())
self._smbfs_driver._remotefsclient = mock.Mock()
self._smbfs_driver._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
self._smbfs_driver._execute = mock.Mock()
self._smbfs_driver.base = self._FAKE_MNT_BASE
self._smbfs_driver._alloc_info_file_path = (
self._FAKE_ALLOCATION_DATA_PATH)
self.context = context.get_admin_context()
self.volume = fake_volume.fake_volume_obj(
self.context,
id='4f711859-4928-4cb7-801a-a50c37ceaccc',
size=self._FAKE_VOLUME_SIZE,
provider_location=self._FAKE_SHARE,
display_name=self._FAKE_VOLUME_NAME,
status='available')
self.snapshot = fake_snapshot.fake_snapshot_obj(
self.context,
id=self._FAKE_SNAPSHOT_ID,
status='available',
volume_size=1)
self.snapshot.volume = self.volume
def _get_fake_allocation_data(self):
return {self._FAKE_SHARE_HASH: {
'total_allocated': self._FAKE_TOTAL_ALLOCATED}}
@mock.patch.object(smbfs, 'open', create=True)
@mock.patch('os.path.exists')
@mock.patch.object(fileutils, 'ensure_tree')
@mock.patch('json.load')
def _test_setup_allocation_data(self, mock_json_load, mock_ensure_tree,
mock_exists, mock_open,
allocation_data_exists=False):
mock_exists.return_value = allocation_data_exists
self._smbfs_driver._update_allocation_data_file = mock.Mock()
self._smbfs_driver._setup_allocation_data()
if allocation_data_exists:
fd = mock_open.return_value.__enter__.return_value
mock_json_load.assert_called_once_with(fd)
self.assertEqual(mock_json_load.return_value,
self._smbfs_driver._allocation_data)
else:
mock_ensure_tree.assert_called_once_with(
os.path.dirname(self._FAKE_ALLOCATION_DATA_PATH))
update_func = self._smbfs_driver._update_allocation_data_file
update_func.assert_called_once_with()
def test_setup_allocation_data_file_unexisting(self):
self._test_setup_allocation_data()
def test_setup_allocation_data_file_existing(self):
self._test_setup_allocation_data(allocation_data_exists=True)
def _test_update_allocation_data(self, virtual_size_gb=None,
volume_exists=True):
self._smbfs_driver._update_allocation_data_file = mock.Mock()
update_func = self._smbfs_driver._update_allocation_data_file
fake_alloc_data = self._get_fake_allocation_data()
if volume_exists:
fake_alloc_data[self._FAKE_SHARE_HASH][
self._FAKE_VOLUME_NAME] = self.volume.size
self._smbfs_driver._allocation_data = fake_alloc_data
self._smbfs_driver.update_disk_allocation_data(self.volume,
virtual_size_gb)
vol_allocated_size = fake_alloc_data[self._FAKE_SHARE_HASH].get(
self._FAKE_VOLUME_NAME, None)
if not virtual_size_gb:
expected_total_allocated = (self._FAKE_TOTAL_ALLOCATED -
self.volume.size)
self.assertIsNone(vol_allocated_size)
else:
expected_total_allocated = (self._FAKE_TOTAL_ALLOCATED +
virtual_size_gb -
self.volume.size)
self.assertEqual(virtual_size_gb, vol_allocated_size)
update_func.assert_called_once_with()
self.assertEqual(
expected_total_allocated,
fake_alloc_data[self._FAKE_SHARE_HASH]['total_allocated'])
def test_update_allocation_data_volume_deleted(self):
self._test_update_allocation_data()
def test_update_allocation_data_volume_extended(self):
self._test_update_allocation_data(
virtual_size_gb=self.volume.size + 1)
def test_update_allocation_data_volume_created(self):
self._test_update_allocation_data(
virtual_size_gb=self.volume.size)
@requires_allocation_data_update(expected_size=None)
def test_delete_volume(self):
drv = self._smbfs_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.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)
@mock.patch('os.path.exists')
@mock.patch.object(image_utils, 'check_qemu_img_version')
def _test_setup(self, mock_check_qemu_img_version,
mock_exists, config, share_config_exists=True):
mock_exists.return_value = share_config_exists
fake_ensure_mounted = mock.MagicMock()
self._smbfs_driver._ensure_shares_mounted = fake_ensure_mounted
self._smbfs_driver.configuration = config
if not (config.smbfs_shares_config and share_config_exists and
config.smbfs_oversub_ratio > 0 and
0 <= config.smbfs_used_ratio <= 1):
self.assertRaises(exception.SmbfsException,
self._smbfs_driver.do_setup,
None)
else:
self._smbfs_driver.do_setup(mock.sentinel.context)
mock_check_qemu_img_version.assert_called_once_with()
self.assertEqual({}, self._smbfs_driver.shares)
fake_ensure_mounted.assert_called_once_with()
def test_setup_missing_shares_config_option(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_shares_config = None
self._test_setup(config=fake_config,
share_config_exists=False)
def test_setup_missing_shares_config_file(self):
self._test_setup(config=self._FAKE_SMBFS_CONFIG,
share_config_exists=False)
def test_setup_invlid_oversub_ratio(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_oversub_ratio = -1
self._test_setup(config=fake_config)
def test_setup_invalid_used_ratio(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_used_ratio = -1
self._test_setup(config=fake_config)
def test_setup_invalid_used_ratio2(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_used_ratio = 1.1
self._test_setup(config=fake_config)
@mock.patch('os.path.exists')
@mock.patch.multiple(smbfs.SmbfsDriver,
_create_windows_image=mock.DEFAULT,
_create_regular_file=mock.DEFAULT,
_create_qcow2_file=mock.DEFAULT,
_create_sparsed_file=mock.DEFAULT,
get_volume_format=mock.DEFAULT,
local_path=mock.DEFAULT,
_set_rw_permissions_for_all=mock.DEFAULT)
def _test_create_volume(self, mock_exists, volume_exists=False,
volume_format=None, use_sparsed_file=False,
**mocks):
self._smbfs_driver.configuration = copy.copy(self._FAKE_SMBFS_CONFIG)
self._smbfs_driver.configuration.smbfs_sparsed_volumes = (
use_sparsed_file)
self._smbfs_driver.get_volume_format.return_value = volume_format
self._smbfs_driver.local_path.return_value = mock.sentinel.vol_path
mock_exists.return_value = volume_exists
if volume_exists:
self.assertRaises(exception.InvalidVolume,
self._smbfs_driver._do_create_volume,
self.volume)
return
self._smbfs_driver._do_create_volume(self.volume)
expected_create_args = [mock.sentinel.vol_path,
self.volume.size]
if volume_format in [self._smbfs_driver._DISK_FORMAT_VHDX,
self._smbfs_driver._DISK_FORMAT_VHD]:
expected_create_args.append(volume_format)
exp_create_method = self._smbfs_driver._create_windows_image
else:
if volume_format == self._smbfs_driver._DISK_FORMAT_QCOW2:
exp_create_method = self._smbfs_driver._create_qcow2_file
elif use_sparsed_file:
exp_create_method = self._smbfs_driver._create_sparsed_file
else:
exp_create_method = self._smbfs_driver._create_regular_file
exp_create_method.assert_called_once_with(*expected_create_args)
mock_set_permissions = self._smbfs_driver._set_rw_permissions_for_all
mock_set_permissions.assert_called_once_with(mock.sentinel.vol_path)
def test_create_existing_volume(self):
self._test_create_volume(volume_exists=True)
def test_create_vhdx(self):
self._test_create_volume(volume_format='vhdx')
def test_create_qcow2(self):
self._test_create_volume(volume_format='qcow2')
def test_create_sparsed(self):
self._test_create_volume(volume_format='raw',
use_sparsed_file=True)
def test_create_regular(self):
self._test_create_volume()
def _test_find_share(self, existing_mounted_shares=True,
eligible_shares=True):
if existing_mounted_shares:
mounted_shares = ('fake_share1', 'fake_share2', 'fake_share3')
else:
mounted_shares = None
self._smbfs_driver._mounted_shares = mounted_shares
self._smbfs_driver._is_share_eligible = mock.Mock(
return_value=eligible_shares)
self._smbfs_driver._get_total_allocated = mock.Mock(
side_effect=[3, 2, 1])
if not mounted_shares:
self.assertRaises(exception.SmbfsNoSharesMounted,
self._smbfs_driver._find_share,
self.volume.size)
elif not eligible_shares:
self.assertRaises(exception.SmbfsNoSuitableShareFound,
self._smbfs_driver._find_share,
self.volume.size)
else:
ret_value = self._smbfs_driver._find_share(
self.volume.size)
# The eligible share with the minimum allocated space
# will be selected
self.assertEqual('fake_share3', ret_value)
def test_find_share(self):
self._test_find_share()
def test_find_share_missing_mounted_shares(self):
self._test_find_share(existing_mounted_shares=False)
def test_find_share_missing_eligible_shares(self):
self._test_find_share(eligible_shares=False)
def _test_is_share_eligible(self, capacity_info, volume_size):
self._smbfs_driver._get_capacity_info = mock.Mock(
return_value=[float(x << 30) for x in capacity_info])
self._smbfs_driver.configuration = self._FAKE_SMBFS_CONFIG
return self._smbfs_driver._is_share_eligible(self._FAKE_SHARE,
volume_size)
def test_share_volume_above_used_ratio(self):
fake_capacity_info = (4, 1, 1)
fake_volume_size = 2
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertFalse(ret_value)
def test_eligible_share(self):
fake_capacity_info = (4, 4, 0)
fake_volume_size = 1
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertTrue(ret_value)
def test_share_volume_above_oversub_ratio(self):
fake_capacity_info = (4, 4, 7)
fake_volume_size = 2
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertFalse(ret_value)
def test_share_reserved_above_oversub_ratio(self):
fake_capacity_info = (4, 4, 10)
fake_volume_size = 1
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertFalse(ret_value)
def test_parse_options(self):
(opt_list,
opt_dict) = self._smbfs_driver.parse_options(
self._FAKE_SHARE_OPTS)
expected_ret = ([], self._FAKE_OPTIONS_DICT)
self.assertEqual(expected_ret, (opt_list, opt_dict))
def test_parse_credentials(self):
fake_smb_options = r'-o user=MyDomain\Administrator,noperm'
expected_flags = '-o username=Administrator,noperm'
flags = self._smbfs_driver.parse_credentials(fake_smb_options)
self.assertEqual(expected_flags, flags)
@mock.patch.object(smbfs.SmbfsDriver, '_get_local_volume_path_template')
@mock.patch.object(smbfs.SmbfsDriver, '_lookup_local_volume_path')
@mock.patch.object(smbfs.SmbfsDriver, 'get_volume_format')
def _test_get_volume_path(self, mock_get_volume_format, mock_lookup_volume,
mock_get_path_template, volume_exists=True):
drv = self._smbfs_driver
mock_get_path_template.return_value = self._FAKE_VOLUME_PATH
volume_format = 'raw'
expected_vol_path = self._FAKE_VOLUME_PATH + '.' + volume_format
mock_lookup_volume.return_value = (
expected_vol_path if volume_exists else None)
mock_get_volume_format.return_value = volume_format
ret_val = drv.local_path(self.volume)
if volume_exists:
self.assertFalse(mock_get_volume_format.called)
else:
mock_get_volume_format.assert_called_once_with(self.volume)
self.assertEqual(expected_vol_path, ret_val)
def test_get_existing_volume_path(self):
self._test_get_volume_path()
def test_get_new_volume_path(self):
self._test_get_volume_path(volume_exists=False)
@mock.patch.object(smbfs.SmbfsDriver, '_local_volume_dir')
def test_get_local_volume_path_template(self, mock_get_local_dir):
mock_get_local_dir.return_value = self._FAKE_MNT_POINT
ret_val = self._smbfs_driver._get_local_volume_path_template(
self.volume)
self.assertEqual(self._FAKE_VOLUME_PATH, ret_val)
@mock.patch('os.path.exists')
def test_lookup_local_volume_path(self, mock_exists):
expected_path = self._FAKE_VOLUME_PATH + '.vhdx'
mock_exists.side_effect = lambda x: x == expected_path
ret_val = self._smbfs_driver._lookup_local_volume_path(
self._FAKE_VOLUME_PATH)
extensions = [''] + [
".%s" % ext
for ext in self._smbfs_driver._SUPPORTED_IMAGE_FORMATS]
possible_paths = [self._FAKE_VOLUME_PATH + ext
for ext in extensions]
mock_exists.assert_has_calls(
[mock.call(path) for path in possible_paths])
self.assertEqual(expected_path, ret_val)
@mock.patch.object(smbfs.SmbfsDriver, '_get_local_volume_path_template')
@mock.patch.object(smbfs.SmbfsDriver, '_lookup_local_volume_path')
@mock.patch.object(smbfs.SmbfsDriver, '_qemu_img_info')
@mock.patch.object(smbfs.SmbfsDriver, '_get_volume_format_spec')
def _mock_get_volume_format(self, mock_get_format_spec, mock_qemu_img_info,
mock_lookup_volume, mock_get_path_template,
qemu_format=False, volume_format='raw',
volume_exists=True):
mock_get_path_template.return_value = self._FAKE_VOLUME_PATH
mock_lookup_volume.return_value = (
self._FAKE_VOLUME_PATH if volume_exists else None)
mock_qemu_img_info.return_value.file_format = volume_format
mock_get_format_spec.return_value = volume_format
ret_val = self._smbfs_driver.get_volume_format(self.volume,
qemu_format)
if volume_exists:
mock_qemu_img_info.assert_called_once_with(self._FAKE_VOLUME_PATH,
self._FAKE_VOLUME_NAME)
self.assertFalse(mock_get_format_spec.called)
else:
mock_get_format_spec.assert_called_once_with(self.volume)
self.assertFalse(mock_qemu_img_info.called)
return ret_val
def test_get_existing_raw_volume_format(self):
fmt = self._mock_get_volume_format()
self.assertEqual('raw', fmt)
def test_get_new_vhd_volume_format(self):
expected_fmt = 'vhd'
fmt = self._mock_get_volume_format(volume_format=expected_fmt,
volume_exists=False)
self.assertEqual(expected_fmt, fmt)
def test_get_new_vhd_legacy_volume_format(self):
img_fmt = 'vhd'
expected_fmt = 'vpc'
ret_val = self._mock_get_volume_format(volume_format=img_fmt,
volume_exists=False,
qemu_format=True)
self.assertEqual(expected_fmt, ret_val)
def test_initialize_connection(self):
self._smbfs_driver.get_active_image_from_info = mock.Mock(
return_value=self._FAKE_VOLUME_NAME)
self._smbfs_driver._get_mount_point_base = mock.Mock(
return_value=self._FAKE_MNT_BASE)
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
self._smbfs_driver.get_volume_format = mock.Mock(
return_value=mock.sentinel.format)
fake_data = {'export': self._FAKE_SHARE,
'format': mock.sentinel.format,
'name': self._FAKE_VOLUME_NAME,
'options': self._FAKE_SHARE_OPTS}
expected = {
'driver_volume_type': 'smbfs',
'data': fake_data,
'mount_point_base': self._FAKE_MNT_BASE}
ret_val = self._smbfs_driver.initialize_connection(
self.volume, None)
self.assertEqual(expected, ret_val)
def _test_extend_volume(self, extend_failed=False, image_format='raw'):
drv = self._smbfs_driver
drv.local_path = mock.Mock(
return_value=self._FAKE_VOLUME_PATH)
drv._check_extend_volume_support = mock.Mock(
return_value=True)
drv._is_file_size_equal = mock.Mock(
return_value=not extend_failed)
drv._qemu_img_info = mock.Mock(
return_value=mock.Mock(file_format=image_format))
drv._delete = mock.Mock()
with mock.patch.object(image_utils, 'resize_image') as fake_resize, \
mock.patch.object(image_utils, 'convert_image') as \
fake_convert:
if extend_failed:
self.assertRaises(exception.ExtendVolumeError,
drv.extend_volume,
self.volume, mock.sentinel.new_size)
else:
drv.extend_volume(self.volume, mock.sentinel.new_size)
if image_format in (drv._DISK_FORMAT_VHDX,
drv._DISK_FORMAT_VHD_LEGACY):
fake_tmp_path = self._FAKE_VOLUME_PATH + '.tmp'
fake_convert.assert_any_call(self._FAKE_VOLUME_PATH,
fake_tmp_path, 'raw')
fake_resize.assert_called_once_with(
fake_tmp_path, mock.sentinel.new_size)
fake_convert.assert_any_call(fake_tmp_path,
self._FAKE_VOLUME_PATH,
image_format)
else:
fake_resize.assert_called_once_with(
self._FAKE_VOLUME_PATH, mock.sentinel.new_size)
@requires_allocation_data_update(expected_size=mock.sentinel.new_size)
def test_extend_volume(self):
self._test_extend_volume()
def test_extend_volume_failed(self):
self._test_extend_volume(extend_failed=True)
@requires_allocation_data_update(expected_size=mock.sentinel.new_size)
def test_extend_vhd_volume(self):
self._test_extend_volume(image_format='vpc')
def _test_check_extend_support(self, has_snapshots=False,
is_eligible=True):
self._smbfs_driver.local_path = mock.Mock(
return_value=self._FAKE_VOLUME_PATH)
if has_snapshots:
active_file_path = self._FAKE_SNAPSHOT_PATH
else:
active_file_path = self._FAKE_VOLUME_PATH
self._smbfs_driver.get_active_image_from_info = mock.Mock(
return_value=active_file_path)
self._smbfs_driver._is_share_eligible = mock.Mock(
return_value=is_eligible)
if has_snapshots:
self.assertRaises(exception.InvalidVolume,
self._smbfs_driver._check_extend_volume_support,
self.volume, 2)
elif not is_eligible:
self.assertRaises(exception.ExtendVolumeError,
self._smbfs_driver._check_extend_volume_support,
self.volume, 2)
else:
self._smbfs_driver._check_extend_volume_support(
self.volume, 2)
self._smbfs_driver._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)
@requires_allocation_data_update(expected_size=_FAKE_VOLUME_SIZE)
@mock.patch.object(remotefs.RemoteFSSnapDriver, 'create_volume')
def test_create_volume_base(self, mock_create_volume):
self._smbfs_driver.create_volume(self.volume)
mock_create_volume.assert_called_once_with(self.volume)
@requires_allocation_data_update(expected_size=_FAKE_VOLUME_SIZE)
@mock.patch.object(smbfs.SmbfsDriver,
'_create_volume_from_snapshot')
def test_create_volume_from_snapshot(self, mock_create_volume):
self._smbfs_driver.create_volume_from_snapshot(self.volume,
self.snapshot)
mock_create_volume.assert_called_once_with(self.volume,
self.snapshot)
@requires_allocation_data_update(expected_size=_FAKE_VOLUME_SIZE)
@mock.patch.object(smbfs.SmbfsDriver, '_create_cloned_volume')
def test_create_cloned_volume(self, mock_create_volume):
self._smbfs_driver.create_cloned_volume(self.volume,
mock.sentinel.src_vol)
mock_create_volume.assert_called_once_with(self.volume,
mock.sentinel.src_vol)
def test_create_volume_from_unavailable_snapshot(self):
self.snapshot.status = 'error'
self.assertRaises(
exception.InvalidSnapshot,
self._smbfs_driver.create_volume_from_snapshot,
self.volume, self.snapshot)
def test_copy_volume_from_snapshot(self):
drv = self._smbfs_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._set_rw_permissions_for_all = mock.Mock()
with mock.patch.object(image_utils, 'convert_image') as (
fake_convert_image):
drv._copy_volume_from_snapshot(
self.snapshot, self.volume,
self.volume.size)
drv._extend_volume.assert_called_once_with(
self.volume, self.volume.size)
fake_convert_image.assert_called_once_with(
self._FAKE_VOLUME_PATH, self._FAKE_VOLUME_PATH[:-1], 'raw')
def test_ensure_mounted(self):
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
self._smbfs_driver._ensure_share_mounted(self._FAKE_SHARE)
self._smbfs_driver._remotefsclient.mount.assert_called_once_with(
self._FAKE_SHARE, self._FAKE_SHARE_OPTS.split())
def _test_copy_image_to_volume(self, wrong_size_after_fetch=False):
drv = self._smbfs_driver
vol_size_bytes = self.volume.size << 30
fake_img_info = mock.MagicMock()
if wrong_size_after_fetch:
fake_img_info.virtual_size = 2 * vol_size_bytes
else:
fake_img_info.virtual_size = vol_size_bytes
drv.get_volume_format = mock.Mock(
return_value=drv._DISK_FORMAT_VHDX)
drv.local_path = mock.Mock(
return_value=self._FAKE_VOLUME_PATH)
drv._do_extend_volume = mock.Mock()
drv.configuration = mock.MagicMock()
drv.configuration.volume_dd_blocksize = (
mock.sentinel.block_size)
with mock.patch.object(image_utils, 'fetch_to_volume_format') as \
fake_fetch, mock.patch.object(image_utils, 'qemu_img_info') as \
fake_qemu_img_info:
fake_qemu_img_info.return_value = fake_img_info
if wrong_size_after_fetch:
self.assertRaises(
exception.ImageUnacceptable,
drv.copy_image_to_volume,
mock.sentinel.context, self.volume,
mock.sentinel.image_service,
mock.sentinel.image_id)
else:
drv.copy_image_to_volume(
mock.sentinel.context, self.volume,
mock.sentinel.image_service,
mock.sentinel.image_id)
fake_fetch.assert_called_once_with(
mock.sentinel.context, mock.sentinel.image_service,
mock.sentinel.image_id, self._FAKE_VOLUME_PATH,
drv._DISK_FORMAT_VHDX,
mock.sentinel.block_size)
drv._do_extend_volume.assert_called_once_with(
self._FAKE_VOLUME_PATH,
self.volume.size,
self.volume.name)
def test_copy_image_to_volume(self):
self._test_copy_image_to_volume()
def test_copy_image_to_volume_wrong_size_after_fetch(self):
self._test_copy_image_to_volume(wrong_size_after_fetch=True)
def test_get_capacity_info(self):
fake_block_size = 4096.0
fake_total_blocks = 1024
fake_avail_blocks = 512
fake_df = ('%s %s %s' % (fake_block_size, fake_total_blocks,
fake_avail_blocks), None)
self._smbfs_driver._get_mount_point_for_share = mock.Mock(
return_value=self._FAKE_MNT_POINT)
self._smbfs_driver._get_total_allocated = mock.Mock(
return_value=self._FAKE_TOTAL_ALLOCATED)
self._smbfs_driver._execute.return_value = fake_df
ret_val = self._smbfs_driver._get_capacity_info(self._FAKE_SHARE)
expected = (fake_block_size * fake_total_blocks,
fake_block_size * fake_avail_blocks,
self._FAKE_TOTAL_ALLOCATED)
self.assertEqual(expected, ret_val)
@ddt.data([False, False],
[True, True],
[False, True])
@ddt.unpack
def test_get_volume_format_spec(self,
volume_meta_contains_fmt,
volume_type_contains_fmt):
self._smbfs_driver.configuration = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_vol_meta_fmt = 'vhd'
fake_vol_type_fmt = 'vhdx'
volume_metadata = {}
volume_type_extra_specs = {}
if volume_meta_contains_fmt:
volume_metadata['volume_format'] = fake_vol_meta_fmt
elif volume_type_contains_fmt:
volume_type_extra_specs['volume_format'] = fake_vol_type_fmt
volume_type = fake_volume.fake_volume_type_obj(self.context)
volume = fake_volume.fake_volume_obj(self.context)
# Optional arguments are not set in _from_db_object,
# so have to set explicitly here
volume.volume_type = volume_type
volume.metadata = volume_metadata
# Same for extra_specs and VolumeType
volume_type.extra_specs = volume_type_extra_specs
resulted_fmt = self._smbfs_driver._get_volume_format_spec(volume)
if volume_meta_contains_fmt:
expected_fmt = fake_vol_meta_fmt
elif volume_type_contains_fmt:
expected_fmt = fake_vol_type_fmt
else:
expected_fmt = self._FAKE_SMBFS_CONFIG.smbfs_default_volume_format
self.assertEqual(expected_fmt, resulted_fmt)

View File

@ -12,8 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import functools
import os
import ddt
import mock
from oslo_utils import units
@ -23,48 +26,87 @@ 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 remotefs
from cinder.volume.drivers.windows import smbfs
def requires_allocation_data_update(expected_size):
def wrapper(func):
@functools.wraps(func)
def inner(inst, *args, **kwargs):
with mock.patch.object(
inst._smbfs_driver,
'update_disk_allocation_data') as fake_update:
func(inst, *args, **kwargs)
fake_update.assert_called_once_with(inst.volume,
expected_size)
return inner
return wrapper
@ddt.ddt
class WindowsSmbFsTestCase(test.TestCase):
_FAKE_SHARE = '//1.2.3.4/share1'
_FAKE_SHARE_HASH = 'db0bf952c1734092b83e8990bd321131'
_FAKE_MNT_BASE = 'c:\openstack\mnt'
_FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, 'fake_hash')
_FAKE_VOLUME_NAME = 'volume-4f711859-4928-4cb7-801a-a50c37ceaccc'
_FAKE_SNAPSHOT_NAME = _FAKE_VOLUME_NAME + '-snapshot.vhdx'
_FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, _FAKE_SHARE_HASH)
_FAKE_VOLUME_ID = '4f711859-4928-4cb7-801a-a50c37ceaccc'
_FAKE_VOLUME_NAME = 'volume-%s.vhdx' % _FAKE_VOLUME_ID
_FAKE_SNAPSHOT_ID = '50811859-4928-4cb7-801a-a50c37ceacba'
_FAKE_SNAPSHOT_NAME = 'volume-%s-%s.vhdx' % (_FAKE_VOLUME_ID,
_FAKE_SNAPSHOT_ID)
_FAKE_SNAPSHOT_PATH = os.path.join(_FAKE_MNT_POINT,
_FAKE_SNAPSHOT_NAME)
_FAKE_TOTAL_SIZE = '2048'
_FAKE_TOTAL_AVAILABLE = '1024'
_FAKE_VOLUME_SIZE = 1
_FAKE_TOTAL_SIZE = 2048
_FAKE_TOTAL_AVAILABLE = 1024
_FAKE_TOTAL_ALLOCATED = 1024
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
_FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT,
_FAKE_VOLUME_NAME + '.vhdx')
_FAKE_VOLUME_NAME)
_FAKE_ALLOCATION_DATA_PATH = os.path.join('fake_dir',
'fake_allocation_data')
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
@mock.patch.object(smbfs, 'utilsfactory')
@mock.patch.object(smbfs, 'remotefs_brick')
def setUp(self, mock_remotefs, mock_utilsfactory):
super(WindowsSmbFsTestCase, self).setUp()
self.context = context.get_admin_context()
self._FAKE_SMBFS_CONFIG = mock.MagicMock(
smbfs_oversub_ratio = 2,
smbfs_used_ratio = 0.5,
smbfs_shares_config = mock.sentinel.share_config_file,
smbfs_default_volume_format = 'vhdx',
smbfs_sparsed_volumes = False)
self._smbfs_driver = smbfs.WindowsSmbfsDriver(
configuration=mock.Mock())
self._smbfs_driver._delete = mock.Mock()
self._smbfs_driver.local_path = mock.Mock(
return_value=self._FAKE_VOLUME_PATH)
self._smbfs_driver._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
self._smbfs_driver.base = self._FAKE_MNT_BASE
self._smbfs_driver._alloc_info_file_path = (
self._FAKE_ALLOCATION_DATA_PATH)
self.volume = self._simple_volume()
self.snapshot = self._simple_snapshot(volume=self.volume)
def _simple_volume(self, **kwargs):
updates = {'id': 'e8d76af4-cbb9-4b70-8e9e-5a133f1a1a66',
'size': 1,
updates = {'id': self._FAKE_VOLUME_ID,
'size': self._FAKE_VOLUME_SIZE,
'provider_location': self._FAKE_SHARE}
updates.update(kwargs)
ctxt = context.get_admin_context()
return fake_volume.fake_volume_obj(ctxt, **updates)
def _simple_snapshot(self, **kwargs):
volume = self._simple_volume()
volume = kwargs.pop('volume', None) or self._simple_volume()
ctxt = context.get_admin_context()
updates = {'id': '35a23942-7625-4683-ad84-144b76e87a80',
updates = {'id': self._FAKE_SNAPSHOT_ID,
'volume_size': volume.size,
'volume_id': volume.id}
updates.update(kwargs)
@ -72,6 +114,381 @@ class WindowsSmbFsTestCase(test.TestCase):
snapshot.volume = volume
return snapshot
@mock.patch('os.path.exists')
@mock.patch.object(image_utils, 'check_qemu_img_version')
def _test_setup(self, mock_check_qemu_img_version,
mock_exists, config, share_config_exists=True):
mock_exists.return_value = share_config_exists
fake_ensure_mounted = mock.MagicMock()
self._smbfs_driver._ensure_shares_mounted = fake_ensure_mounted
self._smbfs_driver.configuration = config
if not (config.smbfs_shares_config and share_config_exists and
config.smbfs_oversub_ratio > 0 and
0 <= config.smbfs_used_ratio <= 1):
self.assertRaises(exception.SmbfsException,
self._smbfs_driver.do_setup,
None)
else:
self._smbfs_driver.do_setup(mock.sentinel.context)
mock_check_qemu_img_version.assert_called_once_with()
self.assertEqual({}, self._smbfs_driver.shares)
fake_ensure_mounted.assert_called_once_with()
def test_initialize_connection(self):
self._smbfs_driver.get_active_image_from_info = mock.Mock(
return_value=self._FAKE_VOLUME_NAME)
self._smbfs_driver._get_mount_point_base = mock.Mock(
return_value=self._FAKE_MNT_BASE)
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
self._smbfs_driver.get_volume_format = mock.Mock(
return_value=mock.sentinel.format)
fake_data = {'export': self._FAKE_SHARE,
'format': mock.sentinel.format,
'name': self._FAKE_VOLUME_NAME,
'options': self._FAKE_SHARE_OPTS}
expected = {
'driver_volume_type': 'smbfs',
'data': fake_data,
'mount_point_base': self._FAKE_MNT_BASE}
ret_val = self._smbfs_driver.initialize_connection(
self.volume, None)
self.assertEqual(expected, ret_val)
def test_setup_missing_shares_config_option(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_shares_config = None
self._test_setup(config=fake_config,
share_config_exists=False)
def test_setup_missing_shares_config_file(self):
self._test_setup(config=self._FAKE_SMBFS_CONFIG,
share_config_exists=False)
def test_setup_invlid_oversub_ratio(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_oversub_ratio = -1
self._test_setup(config=fake_config)
def test_setup_invalid_used_ratio(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_used_ratio = -1
self._test_setup(config=fake_config)
def test_setup_invalid_used_ratio2(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_used_ratio = 1.1
self._test_setup(config=fake_config)
@mock.patch.object(smbfs, 'open', create=True)
@mock.patch('os.path.exists')
@mock.patch.object(smbfs.fileutils, 'ensure_tree')
@mock.patch('json.load')
def _test_setup_allocation_data(self, mock_json_load, mock_ensure_tree,
mock_exists, mock_open,
allocation_data_exists=False):
mock_exists.return_value = allocation_data_exists
self._smbfs_driver._update_allocation_data_file = mock.Mock()
self._smbfs_driver._setup_allocation_data()
if allocation_data_exists:
fd = mock_open.return_value.__enter__.return_value
mock_json_load.assert_called_once_with(fd)
self.assertEqual(mock_json_load.return_value,
self._smbfs_driver._allocation_data)
else:
mock_ensure_tree.assert_called_once_with(
os.path.dirname(self._FAKE_ALLOCATION_DATA_PATH))
update_func = self._smbfs_driver._update_allocation_data_file
update_func.assert_called_once_with()
def test_setup_allocation_data_file_unexisting(self):
self._test_setup_allocation_data()
def test_setup_allocation_data_file_existing(self):
self._test_setup_allocation_data(allocation_data_exists=True)
def _test_update_allocation_data(self, virtual_size_gb=None,
volume_exists=True):
self._smbfs_driver._update_allocation_data_file = mock.Mock()
update_func = self._smbfs_driver._update_allocation_data_file
fake_alloc_data = {
self._FAKE_SHARE_HASH: {
'total_allocated': self._FAKE_TOTAL_ALLOCATED}}
if volume_exists:
fake_alloc_data[self._FAKE_SHARE_HASH][
self.volume.name] = self.volume.size
self._smbfs_driver._allocation_data = fake_alloc_data
self._smbfs_driver.update_disk_allocation_data(self.volume,
virtual_size_gb)
vol_allocated_size = fake_alloc_data[self._FAKE_SHARE_HASH].get(
self.volume.name, None)
if not virtual_size_gb:
expected_total_allocated = (self._FAKE_TOTAL_ALLOCATED -
self.volume.size)
self.assertIsNone(vol_allocated_size)
else:
exp_added = (self.volume.size if not volume_exists
else virtual_size_gb - self.volume.size)
expected_total_allocated = (self._FAKE_TOTAL_ALLOCATED +
exp_added)
self.assertEqual(virtual_size_gb, vol_allocated_size)
update_func.assert_called_once_with()
self.assertEqual(
expected_total_allocated,
fake_alloc_data[self._FAKE_SHARE_HASH]['total_allocated'])
def test_update_allocation_data_volume_deleted(self):
self._test_update_allocation_data()
def test_update_allocation_data_volume_extended(self):
self._test_update_allocation_data(
virtual_size_gb=self.volume.size + 1)
def test_update_allocation_data_volume_created(self):
self._test_update_allocation_data(
virtual_size_gb=self.volume.size,
volume_exists=False)
def _test_find_share(self, existing_mounted_shares=True,
eligible_shares=True):
if existing_mounted_shares:
mounted_shares = ('fake_share1', 'fake_share2', 'fake_share3')
else:
mounted_shares = None
self._smbfs_driver._mounted_shares = mounted_shares
self._smbfs_driver._is_share_eligible = mock.Mock(
return_value=eligible_shares)
self._smbfs_driver._get_total_allocated = mock.Mock(
side_effect=[3, 2, 1])
if not mounted_shares:
self.assertRaises(exception.SmbfsNoSharesMounted,
self._smbfs_driver._find_share,
self.volume.size)
elif not eligible_shares:
self.assertRaises(exception.SmbfsNoSuitableShareFound,
self._smbfs_driver._find_share,
self.volume.size)
else:
ret_value = self._smbfs_driver._find_share(
self.volume.size)
# The eligible share with the minimum allocated space
# will be selected
self.assertEqual('fake_share3', ret_value)
def test_find_share(self):
self._test_find_share()
def test_find_share_missing_mounted_shares(self):
self._test_find_share(existing_mounted_shares=False)
def test_find_share_missing_eligible_shares(self):
self._test_find_share(eligible_shares=False)
def _test_is_share_eligible(self, capacity_info, volume_size):
self._smbfs_driver._get_capacity_info = mock.Mock(
return_value=[float(x << 30) for x in capacity_info])
self._smbfs_driver.configuration = self._FAKE_SMBFS_CONFIG
return self._smbfs_driver._is_share_eligible(self._FAKE_SHARE,
volume_size)
def test_share_volume_above_used_ratio(self):
fake_capacity_info = (4, 1, 1)
fake_volume_size = 2
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertFalse(ret_value)
def test_eligible_share(self):
fake_capacity_info = (4, 4, 0)
fake_volume_size = 1
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertTrue(ret_value)
def test_share_volume_above_oversub_ratio(self):
fake_capacity_info = (4, 4, 7)
fake_volume_size = 2
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertFalse(ret_value)
def test_share_reserved_above_oversub_ratio(self):
fake_capacity_info = (4, 4, 10)
fake_volume_size = 1
ret_value = self._test_is_share_eligible(fake_capacity_info,
fake_volume_size)
self.assertFalse(ret_value)
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_get_local_volume_path_template')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_lookup_local_volume_path')
@mock.patch.object(smbfs.WindowsSmbfsDriver, 'get_volume_format')
def _test_get_volume_path(self, mock_get_volume_format, mock_lookup_volume,
mock_get_path_template, volume_exists=True):
drv = self._smbfs_driver
(mock_get_path_template.return_value,
ext) = os.path.splitext(self._FAKE_VOLUME_PATH)
volume_format = ext.strip('.')
mock_lookup_volume.return_value = (
self._FAKE_VOLUME_PATH if volume_exists else None)
mock_get_volume_format.return_value = volume_format
ret_val = drv.local_path(self.volume)
if volume_exists:
self.assertFalse(mock_get_volume_format.called)
else:
mock_get_volume_format.assert_called_once_with(self.volume)
self.assertEqual(self._FAKE_VOLUME_PATH, ret_val)
def test_get_existing_volume_path(self):
self._test_get_volume_path()
def test_get_new_volume_path(self):
self._test_get_volume_path(volume_exists=False)
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_local_volume_dir')
def test_get_local_volume_path_template(self, mock_get_local_dir):
mock_get_local_dir.return_value = self._FAKE_MNT_POINT
ret_val = self._smbfs_driver._get_local_volume_path_template(
self.volume)
exp_template = os.path.splitext(self._FAKE_VOLUME_PATH)[0]
self.assertEqual(exp_template, ret_val)
@mock.patch('os.path.exists')
def test_lookup_local_volume_path(self, mock_exists):
expected_path = self._FAKE_VOLUME_PATH + '.vhdx'
mock_exists.side_effect = lambda x: x == expected_path
ret_val = self._smbfs_driver._lookup_local_volume_path(
self._FAKE_VOLUME_PATH)
extensions = [
".%s" % ext
for ext in self._smbfs_driver._SUPPORTED_IMAGE_FORMATS]
possible_paths = [self._FAKE_VOLUME_PATH + ext
for ext in extensions]
mock_exists.assert_has_calls(
[mock.call(path) for path in possible_paths])
self.assertEqual(expected_path, ret_val)
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_get_local_volume_path_template')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_lookup_local_volume_path')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_volume_format_spec')
def _test_get_volume_format(self, mock_get_format_spec,
mock_lookup_volume, mock_get_path_template,
qemu_format=False, volume_format='vhdx',
expected_vol_fmt=None,
volume_exists=True):
expected_vol_fmt = expected_vol_fmt or volume_format
vol_path = '%s.%s' % (os.path.splitext(self._FAKE_VOLUME_PATH)[0],
volume_format)
mock_get_path_template.return_value = vol_path
mock_lookup_volume.return_value = (
vol_path if volume_exists else None)
mock_get_format_spec.return_value = volume_format
supported_fmts = self._smbfs_driver._SUPPORTED_IMAGE_FORMATS
if volume_format.lower() not in supported_fmts:
self.assertRaises(exception.SmbfsException,
self._smbfs_driver.get_volume_format,
self.volume,
qemu_format)
else:
ret_val = self._smbfs_driver.get_volume_format(self.volume,
qemu_format)
if volume_exists:
self.assertFalse(mock_get_format_spec.called)
else:
mock_get_format_spec.assert_called_once_with(self.volume)
self.assertEqual(expected_vol_fmt, ret_val)
def test_get_volume_format_invalid_extension(self):
self._test_get_volume_format(volume_format='fake')
def test_get_existing_vhdx_volume_format(self):
self._test_get_volume_format()
def test_get_new_vhd_volume_format(self):
fmt = 'vhd'
self._test_get_volume_format(volume_format=fmt,
volume_exists=False,
expected_vol_fmt=fmt)
def test_get_new_vhd_legacy_volume_format(self):
img_fmt = 'vhd'
expected_fmt = 'vpc'
self._test_get_volume_format(volume_format=img_fmt,
volume_exists=False,
qemu_format=True,
expected_vol_fmt=expected_fmt)
@ddt.data([False, False],
[True, True],
[False, True])
@ddt.unpack
def test_get_volume_format_spec(self,
volume_meta_contains_fmt,
volume_type_contains_fmt):
self._smbfs_driver.configuration = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_vol_meta_fmt = 'vhd'
fake_vol_type_fmt = 'vhdx'
volume_metadata = {}
volume_type_extra_specs = {}
if volume_meta_contains_fmt:
volume_metadata['volume_format'] = fake_vol_meta_fmt
elif volume_type_contains_fmt:
volume_type_extra_specs['volume_format'] = fake_vol_type_fmt
volume_type = fake_volume.fake_volume_type_obj(self.context)
volume = fake_volume.fake_volume_obj(self.context)
# Optional arguments are not set in _from_db_object,
# so have to set explicitly here
volume.volume_type = volume_type
volume.metadata = volume_metadata
# Same for extra_specs and VolumeType
volume_type.extra_specs = volume_type_extra_specs
resulted_fmt = self._smbfs_driver._get_volume_format_spec(volume)
if volume_meta_contains_fmt:
expected_fmt = fake_vol_meta_fmt
elif volume_type_contains_fmt:
expected_fmt = fake_vol_type_fmt
else:
expected_fmt = self._FAKE_SMBFS_CONFIG.smbfs_default_volume_format
self.assertEqual(expected_fmt, resulted_fmt)
@requires_allocation_data_update(expected_size=_FAKE_VOLUME_SIZE)
@mock.patch.object(remotefs.RemoteFSSnapDriver, 'create_volume')
def test_create_volume_base(self, mock_create_volume):
self._smbfs_driver.create_volume(self.volume)
mock_create_volume.assert_called_once_with(self.volume)
def _test_create_volume(self, volume_exists=False, volume_format='vhdx'):
self._smbfs_driver.create_dynamic_vhd = mock.MagicMock()
fake_create = self._smbfs_driver._vhdutils.create_dynamic_vhd
@ -99,6 +516,37 @@ class WindowsSmbFsTestCase(test.TestCase):
def test_create_volume_invalid_volume(self):
self._test_create_volume(volume_format="qcow")
@requires_allocation_data_update(expected_size=None)
def test_delete_volume(self):
drv = self._smbfs_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.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)
def test_ensure_mounted(self):
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
self._smbfs_driver._ensure_share_mounted(self._FAKE_SHARE)
self._smbfs_driver._remotefsclient.mount.assert_called_once_with(
self._FAKE_SHARE, self._FAKE_SHARE_OPTS)
def test_get_capacity_info(self):
self._smbfs_driver._smbutils.get_share_capacity_info.return_value = (
self._FAKE_TOTAL_SIZE, self._FAKE_TOTAL_AVAILABLE)
@ -116,7 +564,7 @@ class WindowsSmbFsTestCase(test.TestCase):
backing_file)
image_info = self._smbfs_driver._qemu_img_info(self._FAKE_VOLUME_PATH)
self.assertEqual(self._FAKE_VOLUME_NAME + '.vhdx',
self.assertEqual(self._FAKE_VOLUME_NAME,
image_info.image)
backing_file_name = backing_file and os.path.basename(backing_file)
self.assertEqual(backing_file_name, image_info.backing_file)
@ -144,6 +592,30 @@ class WindowsSmbFsTestCase(test.TestCase):
fake_create_diff.assert_called_once_with(self._FAKE_SNAPSHOT_PATH,
self._FAKE_VOLUME_PATH)
@requires_allocation_data_update(expected_size=_FAKE_VOLUME_SIZE)
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_create_volume_from_snapshot')
def test_create_volume_from_snapshot(self, mock_create_volume):
self._smbfs_driver.create_volume_from_snapshot(self.volume,
self.snapshot)
mock_create_volume.assert_called_once_with(self.volume,
self.snapshot)
@requires_allocation_data_update(expected_size=_FAKE_VOLUME_SIZE)
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_create_cloned_volume')
def test_create_cloned_volume(self, mock_create_volume):
self._smbfs_driver.create_cloned_volume(self.volume,
mock.sentinel.src_vol)
mock_create_volume.assert_called_once_with(self.volume,
mock.sentinel.src_vol)
def test_create_volume_from_unavailable_snapshot(self):
self.snapshot.status = 'error'
self.assertRaises(
exception.InvalidSnapshot,
self._smbfs_driver.create_volume_from_snapshot,
self.volume, self.snapshot)
def _test_copy_volume_to_image(self, has_parent=False,
volume_format='vhd'):
drv = self._smbfs_driver
@ -249,7 +721,7 @@ class WindowsSmbFsTestCase(test.TestCase):
fake_volume_info = {
snapshot.id: 'fake_snapshot_file_name'}
fake_img_info = mock.MagicMock()
fake_img_info.backing_file = self._FAKE_VOLUME_NAME + '.vhdx'
fake_img_info.backing_file = self._FAKE_VOLUME_NAME
drv._local_path_volume_info = mock.Mock(
return_value=self._FAKE_VOLUME_PATH + '.info')
@ -279,6 +751,6 @@ class WindowsSmbFsTestCase(test.TestCase):
drv = self._smbfs_driver
drv._rebase_img(
self._FAKE_SNAPSHOT_PATH,
self._FAKE_VOLUME_NAME + '.vhdx', 'vhdx')
self._FAKE_VOLUME_NAME, 'vhdx')
drv._vhdutils.reconnect_parent_vhd.assert_called_once_with(
self._FAKE_SNAPSHOT_PATH, self._FAKE_VOLUME_PATH)

View File

@ -1,663 +0,0 @@
# Copyright (c) 2014 Cloudbase Solutions SRL
# 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 decorator
import inspect
import json
import os
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 fileutils
from oslo_utils import units
from cinder import exception
from cinder.i18n import _, _LI, _LW
from cinder.image import image_utils
from cinder import interface
from cinder import utils
from cinder.volume.drivers import remotefs as remotefs_drv
VERSION = '1.1.0'
LOG = logging.getLogger(__name__)
volume_opts = [
cfg.StrOpt('smbfs_shares_config',
default='/etc/cinder/smbfs_shares',
help='File with the list of available smbfs shares.'),
cfg.StrOpt('smbfs_allocation_info_file_path',
default='$state_path/allocation_data',
help=('The path of the automatically generated file containing '
'information about volume disk space allocation.')),
cfg.StrOpt('smbfs_default_volume_format',
default='qcow2',
choices=['raw', 'qcow2', 'vhd', 'vhdx'],
help=('Default format that will be used when creating volumes '
'if no volume format is specified.')),
cfg.BoolOpt('smbfs_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('smbfs_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.FloatOpt('smbfs_oversub_ratio',
default=1.0,
help=('This will compare the allocated to available space on '
'the volume destination. If the ratio exceeds this '
'number, the destination will no longer be valid.')),
cfg.StrOpt('smbfs_mount_point_base',
default='$state_path/mnt',
help=('Base dir containing mount points for smbfs shares.')),
cfg.StrOpt('smbfs_mount_options',
default='noperm,file_mode=0775,dir_mode=0775',
help=('Mount options passed to the smbfs client. See '
'mount.cifs man page for details.')),
]
CONF = cfg.CONF
CONF.register_opts(volume_opts)
def update_allocation_data(delete=False):
@decorator.decorator
def wrapper(func, inst, *args, **kwargs):
ret_val = func(inst, *args, **kwargs)
call_args = inspect.getcallargs(func, inst, *args, **kwargs)
volume = call_args['volume']
requested_size = call_args.get('size_gb', None)
if delete:
allocated_size_gb = None
else:
allocated_size_gb = requested_size or volume.size
inst.update_disk_allocation_data(volume, allocated_size_gb)
return ret_val
return wrapper
@interface.volumedriver
class SmbfsDriver(remotefs_drv.RemoteFSSnapDriver):
"""SMBFS based cinder volume driver."""
SUPPORTED = False
driver_volume_type = 'smbfs'
driver_prefix = 'smbfs'
volume_backend_name = 'Generic_SMBFS'
SHARE_FORMAT_REGEX = r'//.+/.+'
VERSION = VERSION
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Cinder_Jenkins"
_MINIMUM_QEMU_IMG_VERSION = '1.7'
_DISK_FORMAT_VHD = 'vhd'
_DISK_FORMAT_VHD_LEGACY = 'vpc'
_DISK_FORMAT_VHDX = 'vhdx'
_DISK_FORMAT_RAW = 'raw'
_DISK_FORMAT_QCOW2 = 'qcow2'
_SUPPORTED_IMAGE_FORMATS = [_DISK_FORMAT_RAW, _DISK_FORMAT_QCOW2,
_DISK_FORMAT_VHD, _DISK_FORMAT_VHDX]
_VALID_IMAGE_EXTENSIONS = _SUPPORTED_IMAGE_FORMATS
def __init__(self, execute=putils.execute, *args, **kwargs):
self._remotefsclient = None
super(SmbfsDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(volume_opts)
root_helper = utils.get_root_helper()
self.base = getattr(self.configuration,
'smbfs_mount_point_base')
opts = getattr(self.configuration,
'smbfs_mount_options')
self._remotefsclient = remotefs.RemoteFsClient(
'cifs', root_helper, execute=execute,
smbfs_mount_point_base=self.base,
smbfs_mount_options=opts)
self.img_suffix = None
self._alloc_info_file_path = CONF.smbfs_allocation_info_file_path
def _qemu_img_info(self, path, volume_name):
return super(SmbfsDriver, self)._qemu_img_info_base(
path, volume_name, self.configuration.smbfs_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)
fmt = self.get_volume_format(volume)
data = {'export': volume.provider_location,
'format': fmt,
'name': active_file}
if volume.provider_location in self.shares:
data['options'] = self.shares[volume.provider_location]
return {
'driver_volume_type': self.driver_volume_type,
'data': data,
'mount_point_base': self._get_mount_point_base()
}
def do_setup(self, context):
image_utils.check_qemu_img_version(self._MINIMUM_QEMU_IMG_VERSION)
config = self.configuration.smbfs_shares_config
if not config:
msg = (_("SMBFS config file not set (smbfs_shares_config)."))
LOG.error(msg)
raise exception.SmbfsException(msg)
if not os.path.exists(config):
msg = (_("SMBFS config file at %(config)s doesn't exist.") %
{'config': config})
LOG.error(msg)
raise exception.SmbfsException(msg)
if not os.path.isabs(self.base):
msg = _("Invalid mount point base: %s") % self.base
LOG.error(msg)
raise exception.SmbfsException(msg)
if not self.configuration.smbfs_oversub_ratio > 0:
msg = _(
"SMBFS config 'smbfs_oversub_ratio' invalid. Must be > 0: "
"%s") % self.configuration.smbfs_oversub_ratio
LOG.error(msg)
raise exception.SmbfsException(msg)
if not 0 < self.configuration.smbfs_used_ratio <= 1:
msg = _("SMBFS config 'smbfs_used_ratio' invalid. Must be > 0 "
"and <= 1.0: %s") % self.configuration.smbfs_used_ratio
LOG.error(msg)
raise exception.SmbfsException(msg)
self.shares = {} # address : options
self._ensure_shares_mounted()
self._setup_allocation_data()
def _setup_allocation_data(self):
if not os.path.exists(self._alloc_info_file_path):
fileutils.ensure_tree(
os.path.dirname(self._alloc_info_file_path))
self._allocation_data = {}
self._update_allocation_data_file()
else:
with open(self._alloc_info_file_path, 'r') as f:
self._allocation_data = json.load(f)
def update_disk_allocation_data(self, volume, virtual_size_gb=None):
volume_name = volume.name
smbfs_share = volume.provider_location
if smbfs_share:
share_hash = self._get_hash_str(smbfs_share)
else:
return
share_alloc_data = self._allocation_data.get(share_hash, {})
old_virtual_size = share_alloc_data.get(volume_name, 0)
total_allocated = share_alloc_data.get('total_allocated', 0)
if virtual_size_gb:
share_alloc_data[volume_name] = virtual_size_gb
total_allocated += virtual_size_gb - old_virtual_size
elif share_alloc_data.get(volume_name):
# The volume is deleted.
del share_alloc_data[volume_name]
total_allocated -= old_virtual_size
share_alloc_data['total_allocated'] = total_allocated
self._allocation_data[share_hash] = share_alloc_data
self._update_allocation_data_file()
def _update_allocation_data_file(self):
with open(self._alloc_info_file_path, 'w') as f:
json.dump(self._allocation_data, f)
def _get_total_allocated(self, smbfs_share):
share_hash = self._get_hash_str(smbfs_share)
share_alloc_data = self._allocation_data.get(share_hash, {})
total_allocated = share_alloc_data.get('total_allocated', 0) << 30
return float(total_allocated)
def local_path(self, volume):
"""Get volume path (mounted locally fs path) for given volume.
:param volume: volume reference
"""
volume_path_template = self._get_local_volume_path_template(volume)
volume_path = self._lookup_local_volume_path(volume_path_template)
if volume_path:
return volume_path
# The image does not exist, so retrieve the volume format
# in order to build the path.
fmt = self.get_volume_format(volume)
volume_path = volume_path_template + '.' + fmt
return volume_path
def _get_local_volume_path_template(self, volume):
local_dir = self._local_volume_dir(volume)
local_path_template = os.path.join(local_dir, volume.name)
return local_path_template
def _lookup_local_volume_path(self, volume_path_template):
for ext in [''] + self._SUPPORTED_IMAGE_FORMATS:
volume_path = (volume_path_template + '.' + ext
if ext else volume_path_template)
if os.path.exists(volume_path):
return volume_path
def _local_path_volume_info(self, volume):
return '%s%s' % (self.local_path(volume), '.info')
def _get_new_snap_path(self, snapshot):
vol_path = self.local_path(snapshot.volume)
snap_path, ext = os.path.splitext(vol_path)
snap_path += '.' + snapshot.id + ext
return snap_path
def get_volume_format(self, volume, qemu_format=False):
volume_path_template = self._get_local_volume_path_template(volume)
volume_path = self._lookup_local_volume_path(volume_path_template)
if volume_path:
ext = os.path.splitext(volume_path)[1].strip('.').lower()
if ext in self._SUPPORTED_IMAGE_FORMATS:
volume_format = ext
else:
info = self._qemu_img_info(volume_path, volume.name)
volume_format = info.file_format
else:
volume_format = (
self._get_volume_format_spec(volume) or
self.configuration.smbfs_default_volume_format)
if qemu_format and volume_format == self._DISK_FORMAT_VHD:
volume_format = self._DISK_FORMAT_VHD_LEGACY
elif volume_format == self._DISK_FORMAT_VHD_LEGACY:
volume_format = self._DISK_FORMAT_VHD
return volume_format
@remotefs_drv.locked_volume_id_operation
@update_allocation_data(delete=True)
def delete_volume(self, volume):
"""Deletes a logical volume."""
if not volume.provider_location:
LOG.warning(_LW('Volume %s does not have provider_location '
'specified, skipping.'), volume.name)
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)
else:
LOG.debug("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 _create_windows_image(self, volume_path, volume_size, volume_format):
"""Creates a VHD or VHDX file of a given size."""
# vhd is regarded as vpc by qemu
if volume_format == self._DISK_FORMAT_VHD:
volume_format = self._DISK_FORMAT_VHD_LEGACY
self._execute('qemu-img', 'create', '-f', volume_format,
volume_path, str(volume_size * units.Gi),
run_as_root=True)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def create_volume(self, volume):
return super(SmbfsDriver, self).create_volume(volume)
def _do_create_volume(self, volume):
"""Create a volume on given smbfs_share.
:param volume: volume reference
"""
volume_format = self.get_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 in (self._DISK_FORMAT_VHD, self._DISK_FORMAT_VHDX):
self._create_windows_image(volume_path, volume_size,
volume_format)
else:
self.img_suffix = None
if volume_format == self._DISK_FORMAT_QCOW2:
self._create_qcow2_file(volume_path, volume_size)
elif self.configuration.smbfs_sparsed_volumes:
self._create_sparsed_file(volume_path, volume_size)
else:
self._create_regular_file(volume_path, volume_size)
self._set_rw_permissions_for_all(volume_path)
def _get_capacity_info(self, smbfs_share):
"""Calculate available space on the SMBFS share.
:param smbfs_share: example //172.18.194.100/share
"""
mount_point = self._get_mount_point_for_share(smbfs_share)
df, _ = self._execute('stat', '-f', '-c', '%S %b %a', mount_point,
run_as_root=True)
block_size, blocks_total, blocks_avail = map(float, df.split())
total_available = block_size * blocks_avail
total_size = block_size * blocks_total
total_allocated = self._get_total_allocated(smbfs_share)
return total_size, total_available, total_allocated
def _find_share(self, volume_size_in_gib):
"""Choose SMBFS share among available ones for given volume size.
For instances with more than one share that meets the criteria, the
share with the least "allocated" space will be selected.
:param volume_size_in_gib: int size in GB
"""
if not self._mounted_shares:
raise exception.SmbfsNoSharesMounted()
target_share = None
target_share_reserved = 0
for smbfs_share in self._mounted_shares:
if not self._is_share_eligible(smbfs_share, volume_size_in_gib):
continue
total_allocated = self._get_total_allocated(smbfs_share)
if target_share is not None:
if target_share_reserved > total_allocated:
target_share = smbfs_share
target_share_reserved = total_allocated
else:
target_share = smbfs_share
target_share_reserved = total_allocated
if target_share is None:
raise exception.SmbfsNoSuitableShareFound(
volume_size=volume_size_in_gib)
LOG.debug('Selected %s as target smbfs share.', target_share)
return target_share
def _is_share_eligible(self, smbfs_share, volume_size_in_gib):
"""Verifies SMBFS share is eligible to host volume with given size.
First validation step: ratio of actual space (used_space / total_space)
is less than 'smbfs_used_ratio'. Second validation step: apparent space
allocated (differs from actual space used when using sparse files)
and compares the apparent available
space (total_available * smbfs_oversub_ratio) to ensure enough space is
available for the new volume.
:param smbfs_share: smbfs share
:param volume_size_in_gib: int size in GB
"""
used_ratio = self.configuration.smbfs_used_ratio
oversub_ratio = self.configuration.smbfs_oversub_ratio
requested_volume_size = volume_size_in_gib * units.Gi
total_size, total_available, total_allocated = \
self._get_capacity_info(smbfs_share)
apparent_size = max(0, total_size * oversub_ratio)
apparent_available = max(0, apparent_size - total_allocated)
used = (total_size - total_available) / total_size
if used > used_ratio:
LOG.debug('%s is above smbfs_used_ratio.', smbfs_share)
return False
if apparent_available <= requested_volume_size:
LOG.debug('%s is above smbfs_oversub_ratio.', smbfs_share)
return False
if total_allocated / total_size >= oversub_ratio:
LOG.debug('%s reserved space is above smbfs_oversub_ratio.',
smbfs_share)
return False
return True
def _create_snapshot_online(self, snapshot, backing_filename,
new_snap_path):
msg = _("This driver does not support snapshotting in-use volumes.")
raise exception.SmbfsException(msg)
def _delete_snapshot_online(self, context, snapshot, info):
msg = _("This driver does not support deleting in-use snapshots.")
raise exception.SmbfsException(msg)
def _do_create_snapshot(self, snapshot, backing_filename, new_snap_path):
self._check_snapshot_support(snapshot)
super(SmbfsDriver, self)._do_create_snapshot(
snapshot, backing_filename, new_snap_path)
def _check_snapshot_support(self, snapshot):
volume_format = self.get_volume_format(snapshot.volume)
# qemu-img does not yet support differencing vhd/vhdx
if volume_format in (self._DISK_FORMAT_VHD, self._DISK_FORMAT_VHDX):
err_msg = _("Snapshots are not supported for this volume "
"format: %s") % volume_format
raise exception.InvalidVolume(err_msg)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
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, volume.name)
def _do_extend_volume(self, volume_path, size_gb, volume_name):
info = self._qemu_img_info(volume_path, volume_name)
fmt = info.file_format
# Note(lpetrut): as for version 2.0, qemu-img cannot resize
# vhd/x images. For the moment, we'll just use an intermediary
# conversion in order to be able to do the resize.
if fmt in (self._DISK_FORMAT_VHDX, self._DISK_FORMAT_VHD_LEGACY):
temp_image = volume_path + '.tmp'
image_utils.convert_image(volume_path, temp_image,
self._DISK_FORMAT_RAW)
image_utils.resize_image(temp_image, size_gb)
image_utils.convert_image(temp_image, volume_path, fmt)
self._delete(temp_image)
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):
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))
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def create_volume_from_snapshot(self, volume, snapshot):
return self._create_volume_from_snapshot(volume, snapshot)
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("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 = self.get_volume_format(volume, qemu_format=True)
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("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)
self._set_rw_permissions_for_all(self.local_path(volume))
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_format=True)
image_utils.fetch_to_volume_format(
context, image_service, image_id,
self.local_path(volume), volume_format,
self.configuration.volume_dd_blocksize)
self._do_extend_volume(self.local_path(volume),
volume.size,
volume.name)
data = image_utils.qemu_img_info(self.local_path(volume))
virt_size = data.virtual_size / units.Gi
if virt_size != volume.size:
raise exception.ImageUnacceptable(
image_id=image_id,
reason=(_("Expected volume size was %d") % volume.size)
+ (_(" but size is now %d.") % virt_size))
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
return self._create_cloned_volume(volume, src_vref)
def _ensure_share_mounted(self, smbfs_share):
mnt_flags = []
if self.shares.get(smbfs_share) is not None:
mnt_flags = self.shares[smbfs_share]
# The domain name must be removed from the
# user name when using Samba.
mnt_flags = self.parse_credentials(mnt_flags).split()
self._remotefsclient.mount(smbfs_share, mnt_flags)
def parse_options(self, option_str):
opts_dict = {}
opts_list = []
if option_str:
for i in option_str.split():
if i == '-o':
continue
for j in i.split(','):
tmp_opt = j.split('=')
if len(tmp_opt) > 1:
opts_dict[tmp_opt[0]] = tmp_opt[1]
else:
opts_list.append(tmp_opt[0])
return opts_list, opts_dict
def parse_credentials(self, mnt_flags):
options_list, options_dict = self.parse_options(mnt_flags)
username = (options_dict.pop('user', None) or
options_dict.pop('username', None))
if username:
# Remove the Domain from the user name
options_dict['username'] = username.split('\\')[-1]
else:
options_dict['username'] = 'guest'
named_options = ','.join("%s=%s" % (key, val) for (key, val)
in options_dict.items())
options_list = ','.join(options_list)
flags = '-o ' + ','.join([named_options, options_list])
return flags.strip(',')
def _get_volume_format_spec(self, volume):
vol_type = volume.volume_type
extra_specs = {}
if vol_type and vol_type.extra_specs:
extra_specs = vol_type.extra_specs
extra_specs.update(volume.metadata or {})
return (extra_specs.get('volume_format') or
self.configuration.smbfs_default_volume_format)
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

View File

@ -13,10 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import inspect
import json
import os
import sys
import decorator
from os_brick.remotefs import windows_remotefs as remotefs_brick
from os_win import utilsfactory
from oslo_config import cfg
@ -25,56 +27,171 @@ from oslo_utils import fileutils
from oslo_utils import units
from cinder import exception
from cinder.i18n import _, _LI
from cinder.i18n import _, _LI, _LW
from cinder.image import image_utils
from cinder import interface
from cinder.volume.drivers import remotefs as remotefs_drv
from cinder.volume.drivers import smbfs
VERSION = '1.1.0'
LOG = logging.getLogger(__name__)
volume_opts = [
cfg.StrOpt('smbfs_shares_config',
default=r'C:\OpenStack\smbfs_shares.txt',
help='File with the list of available smbfs shares.'),
cfg.StrOpt('smbfs_allocation_info_file_path',
default=r'C:\OpenStack\allocation_data.txt',
help=('The path of the automatically generated file containing '
'information about volume disk space allocation.')),
cfg.StrOpt('smbfs_default_volume_format',
default='vhd',
choices=['vhd', 'vhdx'],
help=('Default format that will be used when creating volumes '
'if no volume format is specified.')),
cfg.BoolOpt('smbfs_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('smbfs_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.FloatOpt('smbfs_oversub_ratio',
default=1.0,
help=('This will compare the allocated to available space on '
'the volume destination. If the ratio exceeds this '
'number, the destination will no longer be valid.')),
cfg.StrOpt('smbfs_mount_point_base',
default=r'C:\OpenStack\_mnt',
help=('Base dir containing mount points for smbfs shares.')),
]
CONF = cfg.CONF
CONF.set_default('smbfs_shares_config', r'C:\OpenStack\smbfs_shares.txt')
CONF.set_default('smbfs_allocation_info_file_path',
r'C:\OpenStack\allocation_data.txt')
CONF.set_default('smbfs_mount_point_base', r'C:\OpenStack\_mnt')
CONF.set_default('smbfs_default_volume_format', 'vhd')
CONF.register_opts(volume_opts)
def update_allocation_data(delete=False):
@decorator.decorator
def wrapper(func, inst, *args, **kwargs):
ret_val = func(inst, *args, **kwargs)
call_args = inspect.getcallargs(func, inst, *args, **kwargs)
volume = call_args['volume']
requested_size = call_args.get('size_gb', None)
if delete:
allocated_size_gb = None
else:
allocated_size_gb = requested_size or volume.size
inst.update_disk_allocation_data(volume, allocated_size_gb)
return ret_val
return wrapper
@interface.volumedriver
class WindowsSmbfsDriver(smbfs.SmbfsDriver):
# NOTE(lpetrut): This driver is currently inhering the Linux SMBFS driver,
# which is being deprecated. This dependency will be removed along with
# the Linux SMBFS driver during Pike.
SUPPORTED = True
class WindowsSmbfsDriver(remotefs_drv.RemoteFSSnapDriver):
VERSION = VERSION
driver_volume_type = 'smbfs'
driver_prefix = 'smbfs'
volume_backend_name = 'Generic_SMBFS'
SHARE_FORMAT_REGEX = r'//.+/.+'
VERSION = VERSION
_DISK_FORMAT_VHD = 'vhd'
_DISK_FORMAT_VHD_LEGACY = 'vpc'
_DISK_FORMAT_VHDX = 'vhdx'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Microsoft_iSCSI_CI"
_MINIMUM_QEMU_IMG_VERSION = '1.6'
_SUPPORTED_IMAGE_FORMATS = [_DISK_FORMAT_VHD, _DISK_FORMAT_VHDX]
_VALID_IMAGE_EXTENSIONS = _SUPPORTED_IMAGE_FORMATS
def __init__(self, *args, **kwargs):
self._remotefsclient = None
super(WindowsSmbfsDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(volume_opts)
self.base = getattr(self.configuration,
'smbfs_mount_point_base',
CONF.smbfs_mount_point_base)
opts = getattr(self.configuration,
'smbfs_mount_options',
CONF.smbfs_mount_options)
self._remotefsclient = remotefs_brick.WindowsRemoteFsClient(
'cifs', root_helper=None, smbfs_mount_point_base=self.base,
smbfs_mount_options=opts, local_path_for_loopback=True)
local_path_for_loopback=True)
self._vhdutils = utilsfactory.get_vhdutils()
self._pathutils = utilsfactory.get_pathutils()
self._smbutils = utilsfactory.get_smbutils()
self._alloc_info_file_path = (
self.configuration.smbfs_allocation_info_file_path)
def do_setup(self, context):
self._check_os_platform()
super(WindowsSmbfsDriver, self).do_setup(context)
image_utils.check_qemu_img_version(self._MINIMUM_QEMU_IMG_VERSION)
config = self.configuration.smbfs_shares_config
if not config:
msg = (_("SMBFS config file not set (smbfs_shares_config)."))
LOG.error(msg)
raise exception.SmbfsException(msg)
if not os.path.exists(config):
msg = (_("SMBFS config file at %(config)s doesn't exist.") %
{'config': config})
LOG.error(msg)
raise exception.SmbfsException(msg)
if not os.path.isabs(self.base):
msg = _("Invalid mount point base: %s") % self.base
LOG.error(msg)
raise exception.SmbfsException(msg)
if not self.configuration.smbfs_oversub_ratio > 0:
msg = _(
"SMBFS config 'smbfs_oversub_ratio' invalid. Must be > 0: "
"%s") % self.configuration.smbfs_oversub_ratio
LOG.error(msg)
raise exception.SmbfsException(msg)
if not 0 < self.configuration.smbfs_used_ratio <= 1:
msg = _("SMBFS config 'smbfs_used_ratio' invalid. Must be > 0 "
"and <= 1.0: %s") % self.configuration.smbfs_used_ratio
LOG.error(msg)
raise exception.SmbfsException(msg)
self.shares = {} # address : options
self._ensure_shares_mounted()
self._setup_allocation_data()
@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)
fmt = self.get_volume_format(volume)
data = {'export': volume.provider_location,
'format': fmt,
'name': active_file}
if volume.provider_location in self.shares:
data['options'] = self.shares[volume.provider_location]
return {
'driver_volume_type': self.driver_volume_type,
'data': data,
'mount_point_base': self._get_mount_point_base()
}
def _check_os_platform(self):
if sys.platform != 'win32':
@ -82,6 +199,196 @@ class WindowsSmbfsDriver(smbfs.SmbfsDriver):
"driver supports only Win32 platforms.") % sys.platform
raise exception.SmbfsException(_msg)
def _setup_allocation_data(self):
if not os.path.exists(self._alloc_info_file_path):
fileutils.ensure_tree(
os.path.dirname(self._alloc_info_file_path))
self._allocation_data = {}
self._update_allocation_data_file()
else:
with open(self._alloc_info_file_path, 'r') as f:
self._allocation_data = json.load(f)
def update_disk_allocation_data(self, volume, virtual_size_gb=None):
volume_name = volume.name
smbfs_share = volume.provider_location
if smbfs_share:
share_hash = self._get_hash_str(smbfs_share)
else:
return
share_alloc_data = self._allocation_data.get(share_hash, {})
old_virtual_size = share_alloc_data.get(volume_name, 0)
total_allocated = share_alloc_data.get('total_allocated', 0)
if virtual_size_gb:
share_alloc_data[volume_name] = virtual_size_gb
total_allocated += virtual_size_gb - old_virtual_size
elif share_alloc_data.get(volume_name):
# The volume is deleted.
del share_alloc_data[volume_name]
total_allocated -= old_virtual_size
share_alloc_data['total_allocated'] = total_allocated
self._allocation_data[share_hash] = share_alloc_data
self._update_allocation_data_file()
def _update_allocation_data_file(self):
with open(self._alloc_info_file_path, 'w') as f:
json.dump(self._allocation_data, f)
def _get_total_allocated(self, smbfs_share):
share_hash = self._get_hash_str(smbfs_share)
share_alloc_data = self._allocation_data.get(share_hash, {})
total_allocated = share_alloc_data.get('total_allocated', 0) << 30
return float(total_allocated)
def _find_share(self, volume_size_in_gib):
"""Choose SMBFS share among available ones for given volume size.
For instances with more than one share that meets the criteria, the
share with the least "allocated" space will be selected.
:param volume_size_in_gib: int size in GB
"""
if not self._mounted_shares:
raise exception.SmbfsNoSharesMounted()
target_share = None
target_share_reserved = 0
for smbfs_share in self._mounted_shares:
if not self._is_share_eligible(smbfs_share, volume_size_in_gib):
continue
total_allocated = self._get_total_allocated(smbfs_share)
if target_share is not None:
if target_share_reserved > total_allocated:
target_share = smbfs_share
target_share_reserved = total_allocated
else:
target_share = smbfs_share
target_share_reserved = total_allocated
if target_share is None:
raise exception.SmbfsNoSuitableShareFound(
volume_size=volume_size_in_gib)
LOG.debug('Selected %s as target smbfs share.', target_share)
return target_share
def _is_share_eligible(self, smbfs_share, volume_size_in_gib):
"""Verifies SMBFS share is eligible to host volume with given size.
First validation step: ratio of actual space (used_space / total_space)
is less than 'smbfs_used_ratio'. Second validation step: apparent space
allocated (differs from actual space used when using sparse files)
and compares the apparent available
space (total_available * smbfs_oversub_ratio) to ensure enough space is
available for the new volume.
:param smbfs_share: smbfs share
:param volume_size_in_gib: int size in GB
"""
used_ratio = self.configuration.smbfs_used_ratio
oversub_ratio = self.configuration.smbfs_oversub_ratio
requested_volume_size = volume_size_in_gib * units.Gi
total_size, total_available, total_allocated = \
self._get_capacity_info(smbfs_share)
apparent_size = max(0, total_size * oversub_ratio)
apparent_available = max(0, apparent_size - total_allocated)
used = (total_size - total_available) / total_size
if used > used_ratio:
LOG.debug('%s is above smbfs_used_ratio.', smbfs_share)
return False
if apparent_available <= requested_volume_size:
LOG.debug('%s is above smbfs_oversub_ratio.', smbfs_share)
return False
if total_allocated / total_size >= oversub_ratio:
LOG.debug('%s reserved space is above smbfs_oversub_ratio.',
smbfs_share)
return False
return True
def local_path(self, volume):
"""Get volume path (mounted locally fs path) for given volume.
:param volume: volume reference
"""
volume_path_template = self._get_local_volume_path_template(volume)
volume_path = self._lookup_local_volume_path(volume_path_template)
if volume_path:
return volume_path
# The image does not exist, so retrieve the volume format
# in order to build the path.
fmt = self.get_volume_format(volume)
volume_path = volume_path_template + '.' + fmt
return volume_path
def _get_local_volume_path_template(self, volume):
local_dir = self._local_volume_dir(volume)
local_path_template = os.path.join(local_dir, volume.name)
return local_path_template
def _lookup_local_volume_path(self, volume_path_template):
for ext in self._SUPPORTED_IMAGE_FORMATS:
volume_path = (volume_path_template + '.' + ext
if ext else volume_path_template)
if os.path.exists(volume_path):
return volume_path
def _get_new_snap_path(self, snapshot):
vol_path = self.local_path(snapshot.volume)
snap_path, ext = os.path.splitext(vol_path)
snap_path += '.' + snapshot.id + ext
return snap_path
def get_volume_format(self, volume, qemu_format=False):
volume_path_template = self._get_local_volume_path_template(volume)
volume_path = self._lookup_local_volume_path(volume_path_template)
if volume_path:
ext = os.path.splitext(volume_path)[1].strip('.').lower()
if ext in self._SUPPORTED_IMAGE_FORMATS:
volume_format = ext
else:
# Hyper-V relies on file extensions so we're enforcing them.
raise exception.SmbfsException(
_("Invalid image file extension: %s") % ext)
else:
volume_format = (
self._get_volume_format_spec(volume) or
self.configuration.smbfs_default_volume_format)
if qemu_format and volume_format == self._DISK_FORMAT_VHD:
volume_format = self._DISK_FORMAT_VHD_LEGACY
elif volume_format == self._DISK_FORMAT_VHD_LEGACY:
volume_format = self._DISK_FORMAT_VHD
return volume_format
def _get_volume_format_spec(self, volume):
vol_type = volume.volume_type
extra_specs = {}
if vol_type and vol_type.extra_specs:
extra_specs = vol_type.extra_specs
extra_specs.update(volume.metadata or {})
return (extra_specs.get('volume_format') or
self.configuration.smbfs_default_volume_format)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def create_volume(self, volume):
return super(WindowsSmbfsDriver, self).create_volume(volume)
def _do_create_volume(self, volume):
volume_path = self.local_path(volume)
volume_format = self.get_volume_format(volume)
@ -91,8 +398,7 @@ class WindowsSmbfsDriver(smbfs.SmbfsDriver):
err_msg = _('File already exists at: %s') % volume_path
raise exception.InvalidVolume(err_msg)
if volume_format not in (self._DISK_FORMAT_VHD,
self._DISK_FORMAT_VHDX):
if volume_format not in self._SUPPORTED_IMAGE_FORMATS:
err_msg = _("Unsupported volume format: %s ") % volume_format
raise exception.InvalidVolume(err_msg)
@ -104,6 +410,28 @@ class WindowsSmbfsDriver(smbfs.SmbfsDriver):
mnt_flags = self.shares[smbfs_share]
self._remotefsclient.mount(smbfs_share, mnt_flags)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data(delete=True)
def delete_volume(self, volume):
"""Deletes a logical volume."""
if not volume.provider_location:
LOG.warning(_LW('Volume %s does not have provider_location '
'specified, skipping.'), volume.name)
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)
else:
LOG.debug("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 _delete(self, path):
fileutils.delete_if_exists(path)
@ -160,10 +488,50 @@ class WindowsSmbfsDriver(smbfs.SmbfsDriver):
self._vhdutils.create_differencing_vhd(new_snap_path,
backing_file_full_path)
def _do_extend_volume(self, volume_path, size_gb, volume_name=None):
def _create_snapshot_online(self, snapshot, backing_filename,
new_snap_path):
msg = _("This driver does not support snapshotting in-use volumes.")
raise exception.SmbfsException(msg)
def _delete_snapshot_online(self, context, snapshot, info):
msg = _("This driver does not support deleting in-use snapshots.")
raise exception.SmbfsException(msg)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def extend_volume(self, volume, size_gb):
LOG.info(_LI('Extending volume %s.'), volume.id)
self._check_extend_volume_support(volume, size_gb)
self._extend_volume(volume, size_gb)
def _extend_volume(self, volume, size_gb):
volume_path = self.local_path(volume)
LOG.info(_LI('Resizing file %(volume_path)s to %(size_gb)sGB.'),
dict(volume_path=volume_path, size_gb=size_gb))
self._vhdutils.resize_vhd(volume_path, size_gb * units.Gi,
is_file_max_size=False)
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))
@remotefs_drv.locked_volume_id_operation
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy the volume to the specified image."""
@ -216,6 +584,17 @@ class WindowsSmbfsDriver(smbfs.SmbfsDriver):
volume.size * units.Gi,
is_file_max_size=False)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def create_volume_from_snapshot(self, volume, snapshot):
return self._create_volume_from_snapshot(volume, snapshot)
@remotefs_drv.locked_volume_id_operation
@update_allocation_data()
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
return self._create_cloned_volume(volume, src_vref)
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
"""Copy data from snapshot to destination volume."""