diff --git a/cinder/opts.py b/cinder/opts.py index 6d957a60298..e2e76461540 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -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, diff --git a/cinder/tests/unit/volume/drivers/test_smbfs.py b/cinder/tests/unit/volume/drivers/test_smbfs.py deleted file mode 100644 index 819cc7ac5a3..00000000000 --- a/cinder/tests/unit/volume/drivers/test_smbfs.py +++ /dev/null @@ -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) diff --git a/cinder/tests/unit/windows/test_smbfs.py b/cinder/tests/unit/windows/test_smbfs.py index c07b40906d2..2689ba35130 100644 --- a/cinder/tests/unit/windows/test_smbfs.py +++ b/cinder/tests/unit/windows/test_smbfs.py @@ -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) diff --git a/cinder/volume/drivers/smbfs.py b/cinder/volume/drivers/smbfs.py deleted file mode 100644 index b774714ca76..00000000000 --- a/cinder/volume/drivers/smbfs.py +++ /dev/null @@ -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 diff --git a/cinder/volume/drivers/windows/smbfs.py b/cinder/volume/drivers/windows/smbfs.py index 72906b83e44..21cb9910a81 100644 --- a/cinder/volume/drivers/windows/smbfs.py +++ b/cinder/volume/drivers/windows/smbfs.py @@ -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."""