From: Mike Perez Date: Wed, 8 Apr 2015 06:06:57 +0000 (-0700) Subject: Revert "Removing Windows drivers" X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=91f5775135b8c8b691d642c0d87229e927299473;p=openstack-build%2Fcinder-build.git Revert "Removing Windows drivers" This reverts commit 85f0814e1a96ea043e8048cf7081f70fa024f20b. Change-Id: I0b078a60e8d9c95d12185d1ff3ffe74fb71f10ab --- diff --git a/cinder/tests/windows/test_smbfs.py b/cinder/tests/windows/test_smbfs.py new file mode 100644 index 000000000..9a7cd16f0 --- /dev/null +++ b/cinder/tests/windows/test_smbfs.py @@ -0,0 +1,320 @@ +# 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 importlib +import os +import sys + +import mock + +from cinder import exception +from cinder.image import image_utils +from cinder import test + + +class WindowsSmbFsTestCase(test.TestCase): + + _FAKE_SHARE = '//1.2.3.4/share1' + _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_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT, + _FAKE_VOLUME_NAME) + _FAKE_SNAPSHOT_PATH = os.path.join(_FAKE_MNT_POINT, + _FAKE_SNAPSHOT_NAME) + _FAKE_TOTAL_SIZE = '2048' + _FAKE_TOTAL_AVAILABLE = '1024' + _FAKE_TOTAL_ALLOCATED = 1024 + _FAKE_VOLUME = {'id': 'e8d76af4-cbb9-4b70-8e9e-5a133f1a1a66', + 'size': 1, + 'provider_location': _FAKE_SHARE} + _FAKE_SNAPSHOT = {'id': '35a23942-7625-4683-ad84-144b76e87a80', + 'volume': _FAKE_VOLUME, + 'volume_size': _FAKE_VOLUME['size']} + _FAKE_SHARE_OPTS = '-o username=Administrator,password=12345' + _FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT, + _FAKE_VOLUME_NAME + '.vhdx') + _FAKE_LISTDIR = [_FAKE_VOLUME_NAME + '.vhd', + _FAKE_VOLUME_NAME + '.vhdx', 'fake_folder'] + + def setUp(self): + super(WindowsSmbFsTestCase, self).setUp() + self._mock_wmi = mock.MagicMock() + + self._platform_patcher = mock.patch('sys.platform', 'win32') + + mock.patch.dict(sys.modules, wmi=self._mock_wmi, + ctypes=self._mock_wmi).start() + + self._platform_patcher.start() + # self._wmi_patcher.start() + self.addCleanup(mock.patch.stopall) + + smbfs = importlib.import_module( + 'cinder.volume.drivers.windows.smbfs') + smbfs.WindowsSmbfsDriver.__init__ = lambda x: None + self._smbfs_driver = smbfs.WindowsSmbfsDriver() + self._smbfs_driver._remotefsclient = mock.Mock() + self._smbfs_driver._delete = mock.Mock() + self._smbfs_driver.local_path = mock.Mock( + return_value=self._FAKE_VOLUME_PATH) + self._smbfs_driver.vhdutils = mock.Mock() + self._smbfs_driver._check_os_platform = mock.Mock() + + 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 + self._smbfs_driver.get_volume_format = mock.Mock( + return_value=volume_format) + + with mock.patch('os.path.exists', new=lambda x: volume_exists): + if volume_exists or volume_format not in ('vhd', 'vhdx'): + self.assertRaises(exception.InvalidVolume, + self._smbfs_driver._do_create_volume, + self._FAKE_VOLUME) + else: + fake_vol_path = self._FAKE_VOLUME_PATH + self._smbfs_driver._do_create_volume(self._FAKE_VOLUME) + fake_create.assert_called_once_with( + fake_vol_path, self._FAKE_VOLUME['size'] << 30) + + def test_create_volume(self): + self._test_create_volume() + + def test_create_existing_volume(self): + self._test_create_volume(True) + + def test_create_volume_invalid_volume(self): + self._test_create_volume(volume_format="qcow") + + def test_get_capacity_info(self): + self._smbfs_driver._remotefsclient.get_capacity_info = mock.Mock( + return_value=(self._FAKE_TOTAL_SIZE, self._FAKE_TOTAL_AVAILABLE)) + self._smbfs_driver._get_total_allocated = mock.Mock( + return_value=self._FAKE_TOTAL_ALLOCATED) + + ret_val = self._smbfs_driver._get_capacity_info(self._FAKE_SHARE) + expected_ret_val = [int(x) for x in [self._FAKE_TOTAL_SIZE, + self._FAKE_TOTAL_AVAILABLE, + self._FAKE_TOTAL_ALLOCATED]] + self.assertEqual(ret_val, expected_ret_val) + + def test_get_total_allocated(self): + fake_listdir = mock.Mock(side_effect=[self._FAKE_LISTDIR, + self._FAKE_LISTDIR[:-1]]) + fake_folder_path = os.path.join(self._FAKE_SHARE, 'fake_folder') + fake_isdir = lambda x: x == fake_folder_path + self._smbfs_driver._remotefsclient.is_symlink = mock.Mock( + return_value=False) + fake_getsize = mock.Mock(return_value=self._FAKE_VOLUME['size']) + self._smbfs_driver.vhdutils.get_vhd_size = mock.Mock( + return_value={'VirtualSize': 1}) + + with mock.patch.multiple('os.path', isdir=fake_isdir, + getsize=fake_getsize): + with mock.patch('os.listdir', fake_listdir): + ret_val = self._smbfs_driver._get_total_allocated( + self._FAKE_SHARE) + self.assertEqual(ret_val, 4) + + def _test_get_img_info(self, backing_file=None): + self._smbfs_driver.vhdutils.get_vhd_parent_path.return_value = ( + backing_file) + + image_info = self._smbfs_driver._qemu_img_info(self._FAKE_VOLUME_PATH) + self.assertEqual(self._FAKE_VOLUME_NAME + '.vhdx', + image_info.image) + backing_file_name = backing_file and os.path.basename(backing_file) + self.assertEqual(backing_file_name, image_info.backing_file) + + def test_get_img_info_without_backing_file(self): + self._test_get_img_info() + + def test_get_snapshot_info(self): + self._test_get_img_info(self._FAKE_VOLUME_PATH) + + def test_create_snapshot(self): + self._smbfs_driver.vhdutils.create_differencing_vhd = ( + mock.Mock()) + self._smbfs_driver._local_volume_dir = mock.Mock( + return_value=self._FAKE_MNT_POINT) + + fake_create_diff = ( + self._smbfs_driver.vhdutils.create_differencing_vhd) + + self._smbfs_driver._do_create_snapshot( + self._FAKE_SNAPSHOT, + os.path.basename(self._FAKE_VOLUME_PATH), + self._FAKE_SNAPSHOT_PATH) + + fake_create_diff.assert_called_once_with(self._FAKE_SNAPSHOT_PATH, + self._FAKE_VOLUME_PATH) + + def _test_copy_volume_to_image(self, has_parent=False, + volume_format='vhd'): + drv = self._smbfs_driver + + fake_image_meta = {'id': 'fake-image-id'} + + if has_parent: + fake_volume_path = self._FAKE_SNAPSHOT_PATH + fake_parent_path = self._FAKE_VOLUME_PATH + else: + fake_volume_path = self._FAKE_VOLUME_PATH + fake_parent_path = None + + if volume_format == drv._DISK_FORMAT_VHD: + fake_volume_path = fake_volume_path[:-1] + + fake_active_image = os.path.basename(fake_volume_path) + + drv.get_active_image_from_info = mock.Mock( + return_value=fake_active_image) + drv._local_volume_dir = mock.Mock( + return_value=self._FAKE_MNT_POINT) + drv.get_volume_format = mock.Mock( + return_value=volume_format) + drv.vhdutils.get_vhd_parent_path.return_value = ( + fake_parent_path) + + with mock.patch.object(image_utils, 'upload_volume') as ( + fake_upload_volume): + drv.copy_volume_to_image( + mock.sentinel.context, self._FAKE_VOLUME, + mock.sentinel.image_service, fake_image_meta) + + expected_conversion = ( + has_parent or volume_format == drv._DISK_FORMAT_VHDX) + + if expected_conversion: + fake_temp_image_name = '%s.temp_image.%s.%s' % ( + self._FAKE_VOLUME['id'], + fake_image_meta['id'], + drv._DISK_FORMAT_VHD) + fake_temp_image_path = os.path.join( + self._FAKE_MNT_POINT, + fake_temp_image_name) + fake_active_image_path = os.path.join( + self._FAKE_MNT_POINT, + fake_active_image) + upload_path = fake_temp_image_path + + drv.vhdutils.convert_vhd.assert_called_once_with( + fake_active_image_path, + fake_temp_image_path) + drv._delete.assert_called_once_with( + fake_temp_image_path) + else: + upload_path = fake_volume_path + + fake_upload_volume.assert_called_once_with( + mock.sentinel.context, mock.sentinel.image_service, + fake_image_meta, upload_path, drv._DISK_FORMAT_VHD) + + def test_copy_volume_to_image_having_snapshot(self): + self._test_copy_volume_to_image(has_parent=True) + + def test_copy_vhdx_volume_to_image(self): + self._test_copy_volume_to_image(volume_format='vhdx') + + def test_copy_vhd_volume_to_image(self): + self._test_copy_volume_to_image(volume_format='vhd') + + def _test_copy_image_to_volume(self, qemu_version=[1, 7]): + drv = self._smbfs_driver + + fake_image_id = 'fake_image_id' + fake_image_service = mock.MagicMock() + fake_image_service.show.return_value = ( + {'id': fake_image_id, 'disk_format': 'raw'}) + + drv.get_volume_format = mock.Mock( + return_value='vhdx') + drv.local_path = mock.Mock( + return_value=self._FAKE_VOLUME_PATH) + drv._local_volume_dir = mock.Mock( + return_value=self._FAKE_MNT_POINT) + drv.get_qemu_version = mock.Mock( + return_value=qemu_version) + 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: + drv.copy_image_to_volume( + mock.sentinel.context, self._FAKE_VOLUME, + fake_image_service, + fake_image_id) + + if qemu_version < [1, 7]: + fake_temp_image_name = '%s.temp_image.%s.vhd' % ( + self._FAKE_VOLUME['id'], + fake_image_id) + fetch_path = os.path.join(self._FAKE_MNT_POINT, + fake_temp_image_name) + fetch_format = 'vpc' + drv.vhdutils.convert_vhd.assert_called_once_with( + fetch_path, self._FAKE_VOLUME_PATH) + drv._delete.assert_called_with(fetch_path) + + else: + fetch_path = self._FAKE_VOLUME_PATH + fetch_format = 'vhdx' + + fake_fetch.assert_called_once_with( + mock.sentinel.context, fake_image_service, fake_image_id, + fetch_path, fetch_format, mock.sentinel.block_size) + + def test_copy_image_to_volume(self): + self._test_copy_image_to_volume() + + def test_copy_image_to_volume_with_conversion(self): + self._test_copy_image_to_volume([1, 5]) + + 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 + '.vhdx' + + 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=mock.sentinel.new_volume_path) + + drv._copy_volume_from_snapshot( + self._FAKE_SNAPSHOT, self._FAKE_VOLUME, + self._FAKE_VOLUME['size']) + + drv._delete.assert_called_once_with(mock.sentinel.new_volume_path) + drv.vhdutils.convert_vhd.assert_called_once_with( + self._FAKE_VOLUME_PATH, + mock.sentinel.new_volume_path) + drv.vhdutils.resize_vhd.assert_called_once_with( + mock.sentinel.new_volume_path, self._FAKE_VOLUME['size'] << 30) + + def test_rebase_img(self): + self._smbfs_driver._rebase_img( + self._FAKE_SNAPSHOT_PATH, + self._FAKE_VOLUME_NAME + '.vhdx', 'vhdx') + self._smbfs_driver.vhdutils.reconnect_parent.assert_called_once_with( + self._FAKE_SNAPSHOT_PATH, self._FAKE_VOLUME_PATH) diff --git a/cinder/tests/windows/test_windows_remotefs.py b/cinder/tests/windows/test_windows_remotefs.py new file mode 100644 index 000000000..26bbcfedb --- /dev/null +++ b/cinder/tests/windows/test_windows_remotefs.py @@ -0,0 +1,164 @@ +# 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 ctypes +import os + +import mock + +from cinder import exception +from cinder import test +from cinder.volume.drivers.windows import remotefs + + +class WindowsRemoteFsTestCase(test.TestCase): + _FAKE_SHARE = '//1.2.3.4/share1' + _FAKE_MNT_BASE = 'C:\OpenStack\mnt' + _FAKE_HASH = 'db0bf952c1734092b83e8990bd321131' + _FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, _FAKE_HASH) + _FAKE_SHARE_OPTS = '-o username=Administrator,password=12345' + _FAKE_OPTIONS_DICT = {'username': 'Administrator', + 'password': '12345'} + + def setUp(self): + super(WindowsRemoteFsTestCase, self).setUp() + + remotefs.ctypes.windll = mock.MagicMock() + remotefs.WindowsRemoteFsClient.__init__ = mock.Mock(return_value=None) + + self._remotefs = remotefs.WindowsRemoteFsClient( + 'cifs', root_helper=None, + smbfs_mount_point_base=self._FAKE_MNT_BASE) + self._remotefs._mount_base = self._FAKE_MNT_BASE + self._remotefs.smb_conn = mock.MagicMock() + self._remotefs.conn_cimv2 = mock.MagicMock() + + def _test_mount_share(self, mount_point_exists=True, is_symlink=True, + mount_base_exists=True): + fake_exists = mock.Mock(return_value=mount_point_exists) + fake_isdir = mock.Mock(return_value=mount_base_exists) + fake_makedirs = mock.Mock() + with mock.patch.multiple('os.path', exists=fake_exists, + isdir=fake_isdir): + with mock.patch('os.makedirs', fake_makedirs): + self._remotefs.is_symlink = mock.Mock( + return_value=is_symlink) + self._remotefs.create_sym_link = mock.MagicMock() + self._remotefs._mount = mock.MagicMock() + fake_norm_path = os.path.abspath(self._FAKE_SHARE) + + if mount_point_exists: + if not is_symlink: + self.assertRaises(exception.SmbfsException, + self._remotefs.mount, + self._FAKE_MNT_POINT, + self._FAKE_OPTIONS_DICT) + else: + self._remotefs.mount(self._FAKE_SHARE, + self._FAKE_OPTIONS_DICT) + if not mount_base_exists: + fake_makedirs.assert_called_once_with( + self._FAKE_MNT_BASE) + self._remotefs._mount.assert_called_once_with( + fake_norm_path, self._FAKE_OPTIONS_DICT) + self._remotefs.create_sym_link.assert_called_once_with( + self._FAKE_MNT_POINT, fake_norm_path) + + def test_mount_linked_share(self): + # The mountpoint contains a symlink targeting the share path + self._test_mount_share(True) + + def test_mount_unlinked_share(self): + self._test_mount_share(False) + + def test_mount_point_exception(self): + # The mountpoint already exists but it is not a symlink + self._test_mount_share(True, False) + + def test_mount_base_missing(self): + # The mount point base dir does not exist + self._test_mount_share(mount_base_exists=False) + + def _test_check_symlink(self, is_symlink=True, python_version=(2, 7), + is_dir=True): + fake_isdir = mock.Mock(return_value=is_dir) + fake_islink = mock.Mock(return_value=is_symlink) + with mock.patch('sys.version_info', python_version): + with mock.patch.multiple('os.path', isdir=fake_isdir, + islink=fake_islink): + if is_symlink: + ret_value = 0x400 + else: + ret_value = 0x80 + fake_get_attributes = mock.Mock(return_value=ret_value) + ctypes.windll.kernel32.GetFileAttributesW = fake_get_attributes + + ret_value = self._remotefs.is_symlink(self._FAKE_MNT_POINT) + if python_version >= (3, 2): + fake_islink.assert_called_once_with(self._FAKE_MNT_POINT) + else: + fake_get_attributes.assert_called_once_with( + self._FAKE_MNT_POINT) + self.assertEqual(ret_value, is_symlink) + + def test_is_symlink(self): + self._test_check_symlink() + + def test_is_not_symlink(self): + self._test_check_symlink(False) + + def test_check_symlink_python_gt_3_2(self): + self._test_check_symlink(python_version=(3, 3)) + + def test_create_sym_link_exception(self): + ctypes.windll.kernel32.CreateSymbolicLinkW.return_value = 0 + self.assertRaises(exception.VolumeBackendAPIException, + self._remotefs.create_sym_link, + self._FAKE_MNT_POINT, self._FAKE_SHARE) + + def _test_check_smb_mapping(self, existing_mappings=False, + share_available=False): + with mock.patch('os.path.exists', lambda x: share_available): + fake_mapping = mock.MagicMock() + if existing_mappings: + fake_mappings = [fake_mapping] + else: + fake_mappings = [] + + self._remotefs.smb_conn.query.return_value = fake_mappings + ret_val = self._remotefs.check_smb_mapping(self._FAKE_SHARE) + + if existing_mappings: + if share_available: + self.assertTrue(ret_val) + else: + fake_mapping.Remove.assert_called_once_with(True, True) + else: + self.assertFalse(ret_val) + + def test_check_mapping(self): + self._test_check_smb_mapping() + + def test_remake_unavailable_mapping(self): + self._test_check_smb_mapping(True, False) + + def test_available_mapping(self): + self._test_check_smb_mapping(True, True) + + def test_mount_smb(self): + fake_create = self._remotefs.smb_conn.Msft_SmbMapping.Create + self._remotefs._mount(self._FAKE_SHARE, {}) + fake_create.assert_called_once_with(UserName=None, + Password=None, + RemotePath=self._FAKE_SHARE) diff --git a/cinder/volume/drivers/windows/remotefs.py b/cinder/volume/drivers/windows/remotefs.py new file mode 100644 index 000000000..6f7f9ae6e --- /dev/null +++ b/cinder/volume/drivers/windows/remotefs.py @@ -0,0 +1,143 @@ +# 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 ctypes +import os +import sys + +if sys.platform == 'win32': + import wmi + +from oslo_log import log as logging +import six + +from cinder.brick.remotefs import remotefs +from cinder import exception +from cinder.i18n import _, _LE, _LI + +LOG = logging.getLogger(__name__) + + +class WindowsRemoteFsClient(remotefs.RemoteFsClient): + _FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 + + def __init__(self, *args, **kwargs): + super(WindowsRemoteFsClient, self).__init__(*args, **kwargs) + self.smb_conn = wmi.WMI(moniker='root/Microsoft/Windows/SMB') + self.conn_cimv2 = wmi.WMI(moniker='root/cimv2') + + def mount(self, export_path, mnt_options=None): + if not os.path.isdir(self._mount_base): + os.makedirs(self._mount_base) + export_hash = self._get_hash_str(export_path) + + norm_path = os.path.abspath(export_path) + mnt_options = mnt_options or {} + + if not self.check_smb_mapping(norm_path): + self._mount(norm_path, mnt_options) + + link_path = os.path.join(self._mount_base, export_hash) + if os.path.exists(link_path): + if not self.is_symlink(link_path): + raise exception.SmbfsException(_("Link path already exists " + "and its not a symlink")) + else: + self.create_sym_link(link_path, norm_path) + + def is_symlink(self, path): + if sys.version_info >= (3, 2): + return os.path.islink(path) + + file_attr = ctypes.windll.kernel32.GetFileAttributesW( + six.text_type(path)) + + return bool(os.path.isdir(path) and ( + file_attr & self._FILE_ATTRIBUTE_REPARSE_POINT)) + + def create_sym_link(self, link, target, target_is_dir=True): + """If target_is_dir is True, a junction will be created. + + NOTE: Juctions only work on same filesystem. + """ + symlink = ctypes.windll.kernel32.CreateSymbolicLinkW + symlink.argtypes = ( + ctypes.c_wchar_p, + ctypes.c_wchar_p, + ctypes.c_ulong, + ) + symlink.restype = ctypes.c_ubyte + retcode = symlink(link, target, target_is_dir) + if retcode == 0: + err_msg = (_("Could not create symbolic link. " + "Link: %(link)s Target %(target)s") + % {'link': link, 'target': target}) + raise exception.VolumeBackendAPIException(err_msg) + + def check_smb_mapping(self, smbfs_share): + mappings = self.smb_conn.query("SELECT * FROM " + "MSFT_SmbMapping " + "WHERE RemotePath='%s'" % + smbfs_share) + + if len(mappings) > 0: + if os.path.exists(smbfs_share): + LOG.debug('Share already mounted: %s' % smbfs_share) + return True + else: + LOG.debug('Share exists but is unavailable: %s ' + % smbfs_share) + for mapping in mappings: + # Due to a bug in the WMI module, getting the output of + # methods returning None will raise an AttributeError + try: + mapping.Remove(True, True) + except AttributeError: + pass + return False + + def _mount(self, smbfs_share, options): + smb_opts = {'RemotePath': smbfs_share} + smb_opts['UserName'] = (options.get('username') or + options.get('user')) + smb_opts['Password'] = (options.get('password') or + options.get('pass')) + + try: + LOG.info(_LI('Mounting share: %s') % smbfs_share) + self.smb_conn.Msft_SmbMapping.Create(**smb_opts) + except wmi.x_wmi as exc: + err_msg = (_( + 'Unable to mount SMBFS share: %(smbfs_share)s ' + 'WMI exception: %(wmi_exc)s' + 'Options: %(options)s') % {'smbfs_share': smbfs_share, + 'options': smb_opts, + 'wmi_exc': exc}) + raise exception.VolumeBackendAPIException(data=err_msg) + + def get_capacity_info(self, smbfs_share): + norm_path = os.path.abspath(smbfs_share) + kernel32 = ctypes.windll.kernel32 + + free_bytes = ctypes.c_ulonglong(0) + total_bytes = ctypes.c_ulonglong(0) + retcode = kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(norm_path), + None, + ctypes.pointer(total_bytes), + ctypes.pointer(free_bytes)) + if retcode == 0: + LOG.error(_LE("Could not get share %s capacity info.") % + smbfs_share) + return 0, 0 + return total_bytes.value, free_bytes.value diff --git a/cinder/volume/drivers/windows/smbfs.py b/cinder/volume/drivers/windows/smbfs.py new file mode 100644 index 000000000..8ec9688ca --- /dev/null +++ b/cinder/volume/drivers/windows/smbfs.py @@ -0,0 +1,268 @@ +# 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 os +import re +import sys + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units + +from cinder import exception +from cinder.i18n import _ +from cinder.image import image_utils +from cinder.openstack.common import fileutils +from cinder import utils +from cinder.volume.drivers import smbfs +from cinder.volume.drivers.windows import remotefs +from cinder.volume.drivers.windows import vhdutils + +VERSION = '1.0.0' + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.set_default('smbfs_shares_config', r'C:\OpenStack\smbfs_shares.txt') +CONF.set_default('smbfs_mount_point_base', r'C:\OpenStack\_mnt') +CONF.set_default('smbfs_default_volume_format', 'vhd') + + +class WindowsSmbfsDriver(smbfs.SmbfsDriver): + VERSION = VERSION + + def __init__(self, *args, **kwargs): + super(WindowsSmbfsDriver, self).__init__(*args, **kwargs) + 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.WindowsRemoteFsClient( + 'cifs', root_helper=None, smbfs_mount_point_base=self.base, + smbfs_mount_options=opts) + self.vhdutils = vhdutils.VHDUtils() + + def do_setup(self, context): + self._check_os_platform() + super(WindowsSmbfsDriver, self).do_setup(context) + + def _check_os_platform(self): + if sys.platform != 'win32': + _msg = _("This system platform (%s) is not supported. This " + "driver supports only Win32 platforms.") % sys.platform + raise exception.SmbfsException(_msg) + + def _do_create_volume(self, volume): + volume_path = self.local_path(volume) + volume_format = self.get_volume_format(volume) + volume_size_bytes = volume['size'] * units.Gi + + if os.path.exists(volume_path): + 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): + err_msg = _("Unsupported volume format: %s ") % volume_format + raise exception.InvalidVolume(err_msg) + + self.vhdutils.create_dynamic_vhd(volume_path, volume_size_bytes) + + def _ensure_share_mounted(self, smbfs_share): + mnt_options = {} + if self.shares.get(smbfs_share) is not None: + mnt_flags = self.shares[smbfs_share] + mnt_options = self.parse_options(mnt_flags)[1] + self._remotefsclient.mount(smbfs_share, mnt_options) + + def _delete(self, path): + fileutils.delete_if_exists(path) + + def _get_capacity_info(self, smbfs_share): + """Calculate available space on the SMBFS share. + + :param smbfs_share: example //172.18.194.100/var/smbfs + """ + total_size, total_available = self._remotefsclient.get_capacity_info( + smbfs_share) + total_allocated = self._get_total_allocated(smbfs_share) + return_value = [total_size, total_available, total_allocated] + LOG.info('Smb share %s Total size %s Total allocated %s' + % (smbfs_share, total_size, total_allocated)) + return [float(x) for x in return_value] + + def _get_total_allocated(self, smbfs_share): + elements = os.listdir(smbfs_share) + total_allocated = 0 + for element in elements: + element_path = os.path.join(smbfs_share, element) + if not self._remotefsclient.is_symlink(element_path): + if "snapshot" in element: + continue + if re.search(r'\.vhdx?$', element): + total_allocated += self.vhdutils.get_vhd_size( + element_path)['VirtualSize'] + continue + if os.path.isdir(element_path): + total_allocated += self._get_total_allocated(element_path) + continue + total_allocated += os.path.getsize(element_path) + + return total_allocated + + def _img_commit(self, snapshot_path): + self.vhdutils.merge_vhd(snapshot_path) + self._delete(snapshot_path) + + def _rebase_img(self, image, backing_file, volume_format): + # Relative path names are not supported in this case. + image_dir = os.path.dirname(image) + backing_file_path = os.path.join(image_dir, backing_file) + self.vhdutils.reconnect_parent(image, backing_file_path) + + def _qemu_img_info(self, path, volume_name=None): + # This code expects to deal only with relative filenames. + # As this method is needed by the upper class and qemu-img does + # not fully support vhdx images, for the moment we'll use Win32 API + # for retrieving image information. + parent_path = self.vhdutils.get_vhd_parent_path(path) + file_format = os.path.splitext(path)[1][1:].lower() + + if parent_path: + backing_file_name = os.path.split(parent_path)[1].lower() + else: + backing_file_name = None + + class ImageInfo(object): + def __init__(self, image, backing_file): + self.image = image + self.backing_file = backing_file + self.file_format = file_format + + return ImageInfo(os.path.basename(path), + backing_file_name) + + def _do_create_snapshot(self, snapshot, backing_file, new_snap_path): + backing_file_full_path = os.path.join( + self._local_volume_dir(snapshot['volume']), + backing_file) + self.vhdutils.create_differencing_vhd(new_snap_path, + backing_file_full_path) + + def _do_extend_volume(self, volume_path, size_gb): + self.vhdutils.resize_vhd(volume_path, size_gb * units.Gi) + + @utils.synchronized('smbfs', external=False) + def copy_volume_to_image(self, context, volume, image_service, image_meta): + """Copy the volume to the specified image.""" + + # If snapshots exist, flatten to a temporary image, and upload it + + active_file = self.get_active_image_from_info(volume) + active_file_path = os.path.join(self._local_volume_dir(volume), + active_file) + backing_file = self.vhdutils.get_vhd_parent_path(active_file_path) + root_file_fmt = self.get_volume_format(volume) + + temp_path = None + + try: + if backing_file or root_file_fmt == self._DISK_FORMAT_VHDX: + temp_file_name = '%s.temp_image.%s.%s' % ( + volume['id'], + image_meta['id'], + self._DISK_FORMAT_VHD) + temp_path = os.path.join(self._local_volume_dir(volume), + temp_file_name) + + self.vhdutils.convert_vhd(active_file_path, temp_path) + upload_path = temp_path + else: + upload_path = active_file_path + + image_utils.upload_volume(context, + image_service, + image_meta, + upload_path, + self._DISK_FORMAT_VHD) + finally: + if temp_path: + self._delete(temp_path) + + 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_meta = image_service.show(context, image_id) + + fetch_format = volume_format + fetch_path = self.local_path(volume) + self._delete(fetch_path) + qemu_version = self.get_qemu_version() + + needs_conversion = False + + if (qemu_version < [1, 7] and ( + volume_format == self._DISK_FORMAT_VHDX and + image_meta['disk_format'] != self._DISK_FORMAT_VHDX)): + needs_conversion = True + fetch_format = 'vpc' + temp_file_name = '%s.temp_image.%s.%s' % ( + volume['id'], + image_meta['id'], + self._DISK_FORMAT_VHD) + fetch_path = os.path.join(self._local_volume_dir(volume), + temp_file_name) + + image_utils.fetch_to_volume_format( + context, image_service, image_id, + fetch_path, fetch_format, + self.configuration.volume_dd_blocksize) + + if needs_conversion: + self.vhdutils.convert_vhd(fetch_path, self.local_path(volume)) + self._delete(fetch_path) + + self.vhdutils.resize_vhd(self.local_path(volume), + volume['size'] * units.Gi) + + def _copy_volume_from_snapshot(self, snapshot, volume, volume_size): + """Copy data from snapshot to destination volume.""" + + LOG.debug("snapshot: %(snap)s, volume: %(vol)s, " + "volume_size: %(size)s" % + {'snap': snapshot['id'], + 'vol': volume['id'], + 'size': snapshot['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']) + + 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_path = os.path.join(vol_dir, img_info.backing_file) + + volume_path = self.local_path(volume) + self._delete(volume_path) + self.vhdutils.convert_vhd(snapshot_path, + volume_path) + self.vhdutils.resize_vhd(volume_path, volume_size * units.Gi)