]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Windows SMB Volume Driver
authorLucian Petrut <lpetrut@cloudbasesolutions.com>
Thu, 10 Jul 2014 13:36:19 +0000 (16:36 +0300)
committerPetrut Lucian <lpetrut@cloudbasesolutions.com>
Tue, 9 Sep 2014 12:08:39 +0000 (12:08 +0000)
Introduces a volume driver which makes use of SMB shares for hosting
volumes as disk images, having a similar workflow with the other NFS
like drivers.

This driver is based on the SMB Volume driver proposed for Linux,
overriding the platform specific methods.

It includes all the features required by the Juno release.

Driver cert results:
http://paste.openstack.org/show/100600/

The online snapshot related tests have been skipped as this
feature is not supported yet.

Change-Id: Idd5964690bca618c11fd116f80dd802d6cc2f31b
Implements: blueprint windows-smbfs-volume-driver

cinder/tests/windows/test_smbfs.py [new file with mode: 0644]
cinder/tests/windows/test_vhdutils.py
cinder/tests/windows/test_windows_remotefs.py [new file with mode: 0644]
cinder/volume/drivers/windows/constants.py
cinder/volume/drivers/windows/remotefs.py [new file with mode: 0644]
cinder/volume/drivers/windows/smbfs.py [new file with mode: 0644]
cinder/volume/drivers/windows/vhdutils.py

diff --git a/cinder/tests/windows/test_smbfs.py b/cinder/tests/windows/test_smbfs.py
new file mode 100644 (file)
index 0000000..9a7cd16
--- /dev/null
@@ -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)
index 2caa03f9e0f4e998f03e3e6c3ed3721f567ec4d9..19be431d88f326c0e1224c6b53434a92614029d7 100644 (file)
@@ -27,9 +27,9 @@ class VHDUtilsTestCase(test.TestCase):
     _FAKE_JOB_PATH = 'fake_job_path'
     _FAKE_VHD_PATH = r'C:\fake\vhd.vhd'
     _FAKE_DEST_PATH = r'C:\fake\destination.vhdx'
+    _FAKE_FILE_HANDLE = 'fake_file_handle'
     _FAKE_RET_VAL = 0
     _FAKE_VHD_SIZE = 1024
-    _FAKE_DEVICE_ID = 'fake_device_id'
 
     def setUp(self):
         super(VHDUtilsTestCase, self).setUp()
@@ -45,6 +45,7 @@ class VHDUtilsTestCase(test.TestCase):
         # references.
         fake_ctypes.byref = lambda x: x
         fake_ctypes.c_wchar_p = lambda x: x
+        fake_ctypes.c_ulong = lambda x: x
 
         mock.patch.multiple(
             'cinder.volume.drivers.windows.vhdutils', ctypes=fake_ctypes,
@@ -53,9 +54,15 @@ class VHDUtilsTestCase(test.TestCase):
             Win32_RESIZE_VIRTUAL_DISK_PARAMETERS=mock.DEFAULT,
             Win32_CREATE_VIRTUAL_DISK_PARAMETERS=mock.DEFAULT,
             Win32_VIRTUAL_STORAGE_TYPE=mock.DEFAULT,
+            Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V1=mock.DEFAULT,
+            Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V2=mock.DEFAULT,
+            Win32_MERGE_VIRTUAL_DISK_PARAMETERS=mock.DEFAULT,
+            Win32_GET_VIRTUAL_DISK_INFO_PARAMETERS=mock.DEFAULT,
+            Win32_SET_VIRTUAL_DISK_INFO_PARAMETERS=mock.DEFAULT,
             create=True).start()
 
-    def _test_convert_vhd(self, convertion_failed=False):
+    def _test_create_vhd(self, src_path=None, max_internal_size=0,
+                         parent_path=None, create_failed=False):
         self._vhdutils._get_device_id_by_path = mock.Mock(
             side_effect=(vhdutils.VIRTUAL_STORAGE_TYPE_DEVICE_VHD,
                          vhdutils.VIRTUAL_STORAGE_TYPE_DEVICE_VHDX))
@@ -66,25 +73,35 @@ class VHDUtilsTestCase(test.TestCase):
         fake_vst = mock.Mock()
         fake_source_vst = mock.Mock()
 
-        vhdutils.Win32_VIRTUAL_STORAGE_TYPE = mock.Mock(
-            side_effect=[fake_vst, None, fake_source_vst])
+        vhdutils.Win32_VIRTUAL_STORAGE_TYPE.side_effect = [
+            fake_vst, None, fake_source_vst]
         vhdutils.virtdisk.CreateVirtualDisk.return_value = int(
-            convertion_failed)
+            create_failed)
 
-        if convertion_failed:
+        if create_failed:
             self.assertRaises(exception.VolumeBackendAPIException,
-                              self._vhdutils.convert_vhd,
-                              self._FAKE_VHD_PATH, self._FAKE_DEST_PATH,
-                              self._FAKE_TYPE)
+                              self._vhdutils._create_vhd,
+                              self._FAKE_DEST_PATH,
+                              constants.VHD_TYPE_DYNAMIC,
+                              src_path=src_path,
+                              max_internal_size=max_internal_size,
+                              parent_path=parent_path)
         else:
-            self._vhdutils.convert_vhd(self._FAKE_VHD_PATH,
-                                       self._FAKE_DEST_PATH,
-                                       self._FAKE_TYPE)
+            self._vhdutils._create_vhd(self._FAKE_DEST_PATH,
+                                       constants.VHD_TYPE_DYNAMIC,
+                                       src_path=src_path,
+                                       max_internal_size=max_internal_size,
+                                       parent_path=parent_path)
 
-        self.assertEqual(vhdutils.VIRTUAL_STORAGE_TYPE_DEVICE_VHDX,
-                         fake_vst.DeviceId)
         self.assertEqual(vhdutils.VIRTUAL_STORAGE_TYPE_DEVICE_VHD,
-                         fake_source_vst.DeviceId)
+                         fake_vst.DeviceId)
+        self.assertEqual(parent_path, fake_params.ParentPath)
+        self.assertEqual(max_internal_size, fake_params.MaximumSize)
+
+        if src_path:
+            self.assertEqual(vhdutils.VIRTUAL_STORAGE_TYPE_DEVICE_VHDX,
+                             fake_source_vst.DeviceId)
+            self.assertEqual(src_path, fake_params.SourcePath)
 
         vhdutils.virtdisk.CreateVirtualDisk.assert_called_with(
             vhdutils.ctypes.byref(fake_vst),
@@ -95,32 +112,47 @@ class VHDUtilsTestCase(test.TestCase):
             vhdutils.ctypes.byref(vhdutils.wintypes.HANDLE()))
         self.assertTrue(self._vhdutils._close.called)
 
-    def test_convert_vhd_successfully(self):
-        self._test_convert_vhd()
+    def test_create_vhd_exception(self):
+        self._test_create_vhd(create_failed=True)
 
-    def test_convert_vhd_exception(self):
-        self._test_convert_vhd(True)
+    def test_create_dynamic_vhd(self):
+        self._test_create_vhd(max_internal_size=1 << 30)
+
+    def test_create_differencing_vhd(self):
+        self._test_create_vhd(parent_path=self._FAKE_VHD_PATH)
+
+    def test_convert_vhd(self):
+        self._test_create_vhd(src_path=self._FAKE_VHD_PATH)
 
     def _test_open(self, open_failed=False):
+        fake_device_id = vhdutils.VIRTUAL_STORAGE_TYPE_DEVICE_VHD
+
         vhdutils.virtdisk.OpenVirtualDisk.return_value = int(open_failed)
+        self._vhdutils._get_device_id_by_path = mock.Mock(
+            return_value=fake_device_id)
 
         fake_vst = vhdutils.Win32_VIRTUAL_STORAGE_TYPE.return_value
+        fake_params = 'fake_params'
+        fake_access_mask = vhdutils.VIRTUAL_DISK_ACCESS_NONE
+        fake_open_flag = vhdutils.OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS
 
         if open_failed:
             self.assertRaises(exception.VolumeBackendAPIException,
                               self._vhdutils._open,
-                              self._FAKE_DEVICE_ID, self._FAKE_VHD_PATH)
+                              self._FAKE_VHD_PATH)
         else:
-            self._vhdutils._open(self._FAKE_DEVICE_ID,
-                                 self._FAKE_VHD_PATH)
+            self._vhdutils._open(self._FAKE_VHD_PATH,
+                                 open_flag=fake_open_flag,
+                                 open_access_mask=fake_access_mask,
+                                 open_params=fake_params)
 
-        vhdutils.virtdisk.OpenVirtualDisk.assert_called_with(
-            vhdutils.ctypes.byref(fake_vst),
-            vhdutils.ctypes.c_wchar_p(self._FAKE_VHD_PATH),
-            vhdutils.VIRTUAL_DISK_ACCESS_ALL,
-            vhdutils.CREATE_VIRTUAL_DISK_FLAG_NONE, 0,
-            vhdutils.ctypes.byref(vhdutils.wintypes.HANDLE()))
-        self.assertEqual(self._FAKE_DEVICE_ID, fake_vst.DeviceId)
+            vhdutils.virtdisk.OpenVirtualDisk.assert_called_with(
+                vhdutils.ctypes.byref(fake_vst),
+                vhdutils.ctypes.c_wchar_p(self._FAKE_VHD_PATH),
+                fake_access_mask, fake_open_flag, fake_params,
+                vhdutils.ctypes.byref(vhdutils.wintypes.HANDLE()))
+
+            self.assertEqual(fake_device_id, fake_vst.DeviceId)
 
     def test_open_success(self):
         self._test_open()
@@ -153,10 +185,8 @@ class VHDUtilsTestCase(test.TestCase):
             vhdutils.Win32_RESIZE_VIRTUAL_DISK_PARAMETERS.return_value)
 
         self._vhdutils._open = mock.Mock(
-            return_value=vhdutils.ctypes.byref(
-                vhdutils.wintypes.HANDLE()))
+            return_value=self._FAKE_FILE_HANDLE)
         self._vhdutils._close = mock.Mock()
-        self._vhdutils._get_device_id_by_path = mock.Mock(return_value=2)
 
         vhdutils.virtdisk.ResizeVirtualDisk.return_value = int(
             resize_failed)
@@ -171,7 +201,7 @@ class VHDUtilsTestCase(test.TestCase):
                                       self._FAKE_VHD_SIZE)
 
         vhdutils.virtdisk.ResizeVirtualDisk.assert_called_with(
-            vhdutils.ctypes.byref(vhdutils.wintypes.HANDLE()),
+            self._FAKE_FILE_HANDLE,
             vhdutils.RESIZE_VIRTUAL_DISK_FLAG_NONE,
             vhdutils.ctypes.byref(fake_params),
             None)
@@ -182,3 +212,146 @@ class VHDUtilsTestCase(test.TestCase):
 
     def test_resize_vhd_failed(self):
         self._test_resize_vhd(resize_failed=True)
+
+    def _test_merge_vhd(self, merge_failed=False):
+        self._vhdutils._open = mock.Mock(
+            return_value=self._FAKE_FILE_HANDLE)
+        self._vhdutils._close = mock.Mock()
+
+        fake_open_params = vhdutils.Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V1()
+        fake_params = vhdutils.Win32_MERGE_VIRTUAL_DISK_PARAMETERS()
+
+        vhdutils.virtdisk.MergeVirtualDisk.return_value = int(
+            merge_failed)
+        vhdutils.Win32_RESIZE_VIRTUAL_DISK_PARAMETERS.return_value = (
+            fake_params)
+
+        if merge_failed:
+            self.assertRaises(exception.VolumeBackendAPIException,
+                              self._vhdutils.merge_vhd,
+                              self._FAKE_VHD_PATH)
+        else:
+            self._vhdutils.merge_vhd(self._FAKE_VHD_PATH)
+
+        self._vhdutils._open.assert_called_once_with(
+            self._FAKE_VHD_PATH,
+            open_params=vhdutils.ctypes.byref(fake_open_params))
+        self.assertEqual(vhdutils.OPEN_VIRTUAL_DISK_VERSION_1,
+                         fake_open_params.Version)
+        self.assertEqual(2, fake_open_params.RWDepth)
+        vhdutils.virtdisk.MergeVirtualDisk.assert_called_with(
+            self._FAKE_FILE_HANDLE,
+            vhdutils.MERGE_VIRTUAL_DISK_FLAG_NONE,
+            vhdutils.ctypes.byref(fake_params),
+            None)
+
+    def test_merge_vhd_success(self):
+        self._test_merge_vhd()
+
+    def test_merge_vhd_failed(self):
+        self._test_merge_vhd(merge_failed=True)
+
+    def _test_get_vhd_info_member(self, get_vhd_info_failed=False):
+        fake_params = vhdutils.Win32_GET_VIRTUAL_DISK_INFO_PARAMETERS()
+        fake_info_size = vhdutils.ctypes.sizeof(fake_params)
+
+        vhdutils.virtdisk.GetVirtualDiskInformation.return_value = (
+            get_vhd_info_failed)
+        self._vhdutils._close = mock.Mock()
+
+        if get_vhd_info_failed:
+            self.assertRaises(exception.VolumeBackendAPIException,
+                              self._vhdutils._get_vhd_info_member,
+                              self._FAKE_VHD_PATH,
+                              vhdutils.GET_VIRTUAL_DISK_INFO_SIZE)
+            self._vhdutils._close.assert_called_with(
+                self._FAKE_VHD_PATH)
+        else:
+            self._vhdutils._get_vhd_info_member(
+                self._FAKE_VHD_PATH,
+                vhdutils.GET_VIRTUAL_DISK_INFO_SIZE)
+
+        vhdutils.virtdisk.GetVirtualDiskInformation.assert_called_with(
+            self._FAKE_VHD_PATH,
+            vhdutils.ctypes.byref(
+                vhdutils.ctypes.c_ulong(fake_info_size)),
+            vhdutils.ctypes.byref(fake_params), 0)
+
+    def test_get_vhd_info_member_success(self):
+        self._test_get_vhd_info_member()
+
+    def test_get_vhd_info_member_failed(self):
+        self._test_get_vhd_info_member(get_vhd_info_failed=True)
+
+    def test_get_vhd_info(self):
+        fake_vhd_info = {'VirtualSize': self._FAKE_VHD_SIZE}
+        fake_info_member = vhdutils.GET_VIRTUAL_DISK_INFO_SIZE
+
+        self._vhdutils._open = mock.Mock(
+            return_value=self._FAKE_FILE_HANDLE)
+        self._vhdutils._close = mock.Mock()
+        self._vhdutils._get_vhd_info_member = mock.Mock(
+            return_value=fake_vhd_info)
+
+        ret_val = self._vhdutils.get_vhd_info(self._FAKE_VHD_PATH,
+                                              [fake_info_member])
+
+        self.assertEqual(fake_vhd_info, ret_val)
+        self._vhdutils._open.assert_called_once_with(
+            self._FAKE_VHD_PATH,
+            open_access_mask=vhdutils.VIRTUAL_DISK_ACCESS_GET_INFO)
+        self._vhdutils._get_vhd_info_member.assert_called_with(
+            self._FAKE_FILE_HANDLE, fake_info_member)
+        self._vhdutils._close.assert_called_once()
+
+    def test_parse_vhd_info(self):
+        fake_physical_size = self._FAKE_VHD_SIZE + 1
+        fake_info_member = vhdutils.GET_VIRTUAL_DISK_INFO_SIZE
+        fake_info = mock.Mock()
+        fake_info.VhdInfo.Size._fields_ = [
+            ("VirtualSize", vhdutils.wintypes.ULARGE_INTEGER),
+            ("PhysicalSize", vhdutils.wintypes.ULARGE_INTEGER)]
+        fake_info.VhdInfo.Size.VirtualSize = self._FAKE_VHD_SIZE
+        fake_info.VhdInfo.Size.PhysicalSize = fake_physical_size
+
+        ret_val = self._vhdutils._parse_vhd_info(fake_info, fake_info_member)
+        expected = {'VirtualSize': self._FAKE_VHD_SIZE,
+                    'PhysicalSize': fake_physical_size}
+
+        self.assertEqual(expected, ret_val)
+
+    def _test_reconnect_parent(self, reconnect_failed=False):
+        fake_params = vhdutils.Win32_SET_VIRTUAL_DISK_INFO_PARAMETERS()
+        fake_open_params = vhdutils.Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V2()
+
+        self._vhdutils._open = mock.Mock(return_value=self._FAKE_FILE_HANDLE)
+        self._vhdutils._close = mock.Mock()
+        vhdutils.virtdisk.SetVirtualDiskInformation.return_value = int(
+            reconnect_failed)
+
+        if reconnect_failed:
+            self.assertRaises(exception.VolumeBackendAPIException,
+                              self._vhdutils.reconnect_parent,
+                              self._FAKE_VHD_PATH, self._FAKE_DEST_PATH)
+
+        else:
+            self._vhdutils.reconnect_parent(self._FAKE_VHD_PATH,
+                                            self._FAKE_DEST_PATH)
+
+        self._vhdutils._open.assert_called_once_with(
+            self._FAKE_VHD_PATH,
+            open_flag=vhdutils.OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS,
+            open_access_mask=vhdutils.VIRTUAL_DISK_ACCESS_NONE,
+            open_params=vhdutils.ctypes.byref(fake_open_params))
+        self.assertEqual(vhdutils.OPEN_VIRTUAL_DISK_VERSION_2,
+                         fake_open_params.Version)
+        self.assertFalse(fake_open_params.GetInfoOnly)
+        vhdutils.virtdisk.SetVirtualDiskInformation.assert_called_once_with(
+            self._FAKE_FILE_HANDLE, vhdutils.ctypes.byref(fake_params))
+        self.assertEqual(self._FAKE_DEST_PATH, fake_params.ParentFilePath)
+
+    def test_reconnect_parent_success(self):
+        self._test_reconnect_parent()
+
+    def test_reconnect_parent_failed(self):
+        self._test_reconnect_parent(reconnect_failed=True)
diff --git a/cinder/tests/windows/test_windows_remotefs.py b/cinder/tests/windows/test_windows_remotefs.py
new file mode 100644 (file)
index 0000000..26bbcfe
--- /dev/null
@@ -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)
index 6616d3fabeed3fb7ceb7c4b6c1e8ababda0bddbf..457f6bfe263d30a5d3f43f1e46138e7d0fbfad18 100644 (file)
@@ -14,3 +14,4 @@
 
 VHD_TYPE_FIXED = 2
 VHD_TYPE_DYNAMIC = 3
+VHD_TYPE_DIFFERENCING = 4
diff --git a/cinder/volume/drivers/windows/remotefs.py b/cinder/volume/drivers/windows/remotefs.py
new file mode 100644 (file)
index 0000000..ff22214
--- /dev/null
@@ -0,0 +1,140 @@
+#  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 cinder.brick.remotefs import remotefs
+from cinder import exception
+from cinder.openstack.common.gettextutils import _
+from cinder.openstack.common import log as logging
+
+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(unicode(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(_('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(_("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 (file)
index 0000000..275e079
--- /dev/null
@@ -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 cinder import exception
+from cinder.image import image_utils
+from cinder.openstack.common import fileutils
+from cinder.openstack.common.gettextutils import _
+from cinder.openstack.common import log as logging
+from cinder.openstack.common import units
+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):
+        # 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)
index b43136095c827550fcc5d46697fcc6b139106b82..a1cc6ab0d7f9d572bf8876afd4db9936e218e4f0 100644 (file)
@@ -22,6 +22,9 @@ See "Download the Specifications Without Registering"
 
 Official VHDX format specs can be retrieved at:
 http://www.microsoft.com/en-us/download/details.aspx?id=34750
+
+VHD related Win32 API reference:
+http://msdn.microsoft.com/en-us/library/windows/desktop/dd323700.aspx
 """
 import ctypes
 import os
@@ -49,7 +52,7 @@ if os.name == 'nt':
 
     class Win32_VIRTUAL_STORAGE_TYPE(ctypes.Structure):
         _fields_ = [
-            ('DeviceId', wintypes.DWORD),
+            ('DeviceId', wintypes.ULONG),
             ('VendorId', Win32_GUID)
         ]
 
@@ -59,6 +62,26 @@ if os.name == 'nt':
             ('NewSize', ctypes.c_ulonglong)
         ]
 
+    class Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V1(ctypes.Structure):
+        _fields_ = [
+            ('Version', wintypes.DWORD),
+            ('RWDepth', ctypes.c_ulong),
+        ]
+
+    class Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V2(ctypes.Structure):
+        _fields_ = [
+            ('Version', wintypes.DWORD),
+            ('GetInfoOnly', wintypes.BOOL),
+            ('ReadOnly', wintypes.BOOL),
+            ('ResiliencyGuid', Win32_GUID)
+        ]
+
+    class Win32_MERGE_VIRTUAL_DISK_PARAMETERS(ctypes.Structure):
+        _fields_ = [
+            ('Version', wintypes.DWORD),
+            ('MergeDepth', ctypes.c_ulong)
+        ]
+
     class Win32_CREATE_VIRTUAL_DISK_PARAMETERS(ctypes.Structure):
         _fields_ = [
             ('Version', wintypes.DWORD),
@@ -75,19 +98,71 @@ if os.name == 'nt':
             ('ResiliencyGuid', Win32_GUID)
         ]
 
+    class Win32_SIZE(ctypes.Structure):
+        _fields_ = [("VirtualSize", wintypes.ULARGE_INTEGER),
+                    ("PhysicalSize", wintypes.ULARGE_INTEGER),
+                    ("BlockSize", wintypes.ULONG),
+                    ("SectorSize", wintypes.ULONG)]
+
+    class Win32_PARENT_LOCATION(ctypes.Structure):
+        _fields_ = [('ParentResolved', wintypes.BOOL),
+                    ('ParentLocationBuffer', wintypes.WCHAR * 512)]
+
+    class Win32_PHYSICAL_DISK(ctypes.Structure):
+        _fields_ = [("LogicalSectorSize", wintypes.ULONG),
+                    ("PhysicalSectorSize", wintypes.ULONG),
+                    ("IsRemote", wintypes.BOOL)]
+
+    class Win32_VHD_INFO(ctypes.Union):
+        _fields_ = [("Size", Win32_SIZE),
+                    ("Identifier", Win32_GUID),
+                    ("ParentLocation", Win32_PARENT_LOCATION),
+                    ("ParentIdentifier", Win32_GUID),
+                    ("ParentTimestamp", wintypes.ULONG),
+                    ("VirtualStorageType", Win32_VIRTUAL_STORAGE_TYPE),
+                    ("ProviderSubtype", wintypes.ULONG),
+                    ("Is4kAligned", wintypes.BOOL),
+                    ("PhysicalDisk", Win32_PHYSICAL_DISK),
+                    ("VhdPhysicalSectorSize", wintypes.ULONG),
+                    ("SmallestSafeVirtualSize",
+                        wintypes.ULARGE_INTEGER),
+                    ("FragmentationPercentage", wintypes.ULONG)]
+
+    class Win32_GET_VIRTUAL_DISK_INFO_PARAMETERS(ctypes.Structure):
+        _fields_ = [("VERSION", ctypes.wintypes.UINT),
+                    ("VhdInfo", Win32_VHD_INFO)]
+
+    class Win32_SET_VIRTUAL_DISK_INFO_PARAMETERS(ctypes.Structure):
+        _fields_ = [
+            ('Version', wintypes.DWORD),
+            ('ParentFilePath', wintypes.LPCWSTR)
+        ]
+
+
 VIRTUAL_STORAGE_TYPE_DEVICE_ISO = 1
 VIRTUAL_STORAGE_TYPE_DEVICE_VHD = 2
 VIRTUAL_STORAGE_TYPE_DEVICE_VHDX = 3
 VIRTUAL_DISK_ACCESS_NONE = 0
 VIRTUAL_DISK_ACCESS_ALL = 0x003f0000
 VIRTUAL_DISK_ACCESS_CREATE = 0x00100000
+VIRTUAL_DISK_ACCESS_GET_INFO = 0x80000
 OPEN_VIRTUAL_DISK_FLAG_NONE = 0
+OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS = 1
+OPEN_VIRTUAL_DISK_VERSION_1 = 1
+OPEN_VIRTUAL_DISK_VERSION_2 = 2
 RESIZE_VIRTUAL_DISK_FLAG_NONE = 0
 RESIZE_VIRTUAL_DISK_VERSION_1 = 1
 CREATE_VIRTUAL_DISK_VERSION_2 = 2
 CREATE_VHD_PARAMS_DEFAULT_BLOCK_SIZE = 0
 CREATE_VIRTUAL_DISK_FLAG_NONE = 0
 CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION = 1
+MERGE_VIRTUAL_DISK_VERSION_1 = 1
+MERGE_VIRTUAL_DISK_FLAG_NONE = 0x00000000
+GET_VIRTUAL_DISK_INFO_SIZE = 1
+GET_VIRTUAL_DISK_INFO_PARENT_LOCATION = 3
+GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE = 6
+GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE = 7
+SET_VIRTUAL_DISK_INFO_PARENT_PATH = 1
 
 
 class VHDUtils(object):
@@ -101,6 +176,13 @@ class VHDUtils(object):
                 CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION),
             constants.VHD_TYPE_DYNAMIC: CREATE_VIRTUAL_DISK_FLAG_NONE
         }
+        self._vhd_info_members = {
+            GET_VIRTUAL_DISK_INFO_SIZE: 'Size',
+            GET_VIRTUAL_DISK_INFO_PARENT_LOCATION: 'ParentLocation',
+            GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE:
+                'VirtualStorageType',
+            GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE: 'ProviderSubtype',
+        }
 
         if os.name == 'nt':
             self._msft_vendor_id = (
@@ -116,17 +198,23 @@ class VHDUtils(object):
         guid.Data4 = ByteArray8(0x90, 0x1f, 0x71, 0x41, 0x5a, 0x66, 0x34, 0x5b)
         return guid
 
-    def _open(self, device_id, vhd_path):
+    def _open(self, vhd_path, open_flag=OPEN_VIRTUAL_DISK_FLAG_NONE,
+              open_access_mask=VIRTUAL_DISK_ACCESS_ALL,
+              open_params=0):
+        device_id = self._get_device_id_by_path(vhd_path)
+
         vst = Win32_VIRTUAL_STORAGE_TYPE()
         vst.DeviceId = device_id
         vst.VendorId = self._msft_vendor_id
 
         handle = wintypes.HANDLE()
+
         ret_val = virtdisk.OpenVirtualDisk(ctypes.byref(vst),
                                            ctypes.c_wchar_p(vhd_path),
-                                           VIRTUAL_DISK_ACCESS_ALL,
-                                           OPEN_VIRTUAL_DISK_FLAG_NONE,
-                                           0, ctypes.byref(handle))
+                                           open_access_mask,
+                                           open_flag,
+                                           open_params,
+                                           ctypes.byref(handle))
         if ret_val:
             raise exception.VolumeBackendAPIException(
                 _("Opening virtual disk failed with error: %s") % ret_val)
@@ -144,8 +232,7 @@ class VHDUtils(object):
         return device_id
 
     def resize_vhd(self, vhd_path, new_max_size):
-        device_id = self._get_device_id_by_path(vhd_path)
-        handle = self._open(device_id, vhd_path)
+        handle = self._open(vhd_path)
 
         params = Win32_RESIZE_VIRTUAL_DISK_PARAMETERS()
         params.Version = RESIZE_VIRTUAL_DISK_VERSION_1
@@ -157,41 +244,66 @@ class VHDUtils(object):
             ctypes.byref(params),
             None)
         self._close(handle)
-
         if ret_val:
             raise exception.VolumeBackendAPIException(
                 _("Virtual disk resize failed with error: %s") % ret_val)
 
-    def convert_vhd(self, src, dest, vhd_type):
-        src_device_id = self._get_device_id_by_path(src)
-        dest_device_id = self._get_device_id_by_path(dest)
+    def merge_vhd(self, vhd_path):
+        open_params = Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V1()
+        open_params.Version = OPEN_VIRTUAL_DISK_VERSION_1
+        open_params.RWDepth = 2
+
+        handle = self._open(vhd_path,
+                            open_params=ctypes.byref(open_params))
+
+        params = Win32_MERGE_VIRTUAL_DISK_PARAMETERS()
+        params.Version = MERGE_VIRTUAL_DISK_VERSION_1
+        params.MergeDepth = 1
+
+        ret_val = virtdisk.MergeVirtualDisk(
+            handle,
+            MERGE_VIRTUAL_DISK_FLAG_NONE,
+            ctypes.byref(params),
+            None)
+        self._close(handle)
+        if ret_val:
+            raise exception.VolumeBackendAPIException(
+                _("Virtual disk merge failed with error: %s") % ret_val)
+
+    def _create_vhd(self, new_vhd_path, new_vhd_type, src_path=None,
+                    max_internal_size=0, parent_path=None):
+        new_device_id = self._get_device_id_by_path(new_vhd_path)
 
         vst = Win32_VIRTUAL_STORAGE_TYPE()
-        vst.DeviceId = dest_device_id
+        vst.DeviceId = new_device_id
         vst.VendorId = self._msft_vendor_id
 
         params = Win32_CREATE_VIRTUAL_DISK_PARAMETERS()
         params.Version = CREATE_VIRTUAL_DISK_VERSION_2
         params.UniqueId = Win32_GUID()
-        params.MaximumSize = 0
         params.BlockSizeInBytes = CREATE_VHD_PARAMS_DEFAULT_BLOCK_SIZE
         params.SectorSizeInBytes = 0x200
         params.PhysicalSectorSizeInBytes = 0x200
-        params.ParentPath = None
-        params.SourcePath = src
         params.OpenFlags = OPEN_VIRTUAL_DISK_FLAG_NONE
-        params.ParentVirtualStorageType = Win32_VIRTUAL_STORAGE_TYPE()
-        params.SourceVirtualStorageType = Win32_VIRTUAL_STORAGE_TYPE()
-        params.SourceVirtualStorageType.DeviceId = src_device_id
-        params.SourceVirtualStorageType.VendorId = self._msft_vendor_id
         params.ResiliencyGuid = Win32_GUID()
+        params.MaximumSize = max_internal_size
+        params.ParentPath = parent_path
+        params.ParentVirtualStorageType = Win32_VIRTUAL_STORAGE_TYPE()
+
+        if src_path:
+            src_device_id = self._get_device_id_by_path(src_path)
+            params.SourcePath = src_path
+            params.SourceVirtualStorageType = Win32_VIRTUAL_STORAGE_TYPE()
+            params.SourceVirtualStorageType.DeviceId = src_device_id
+            params.SourceVirtualStorageType.VendorId = self._msft_vendor_id
 
         handle = wintypes.HANDLE()
-        create_virtual_disk_flag = self.create_virtual_disk_flags.get(vhd_type)
+        create_virtual_disk_flag = self.create_virtual_disk_flags.get(
+            new_vhd_type)
 
         ret_val = virtdisk.CreateVirtualDisk(
             ctypes.byref(vst),
-            ctypes.c_wchar_p(dest),
+            ctypes.c_wchar_p(new_vhd_path),
             VIRTUAL_DISK_ACCESS_NONE,
             None,
             create_virtual_disk_flag,
@@ -203,4 +315,109 @@ class VHDUtils(object):
 
         if ret_val:
             raise exception.VolumeBackendAPIException(
-                _("Virtual disk conversion failed with error: %s") % ret_val)
+                _("Virtual disk creation failed with error: %s") % ret_val)
+
+    def get_vhd_info(self, vhd_path, info_members=None):
+        vhd_info = {}
+        info_members = info_members or self._vhd_info_members
+
+        handle = self._open(vhd_path,
+                            open_access_mask=VIRTUAL_DISK_ACCESS_GET_INFO)
+
+        for member in info_members:
+            info = self._get_vhd_info_member(handle, member)
+            vhd_info = dict(vhd_info.items() + info.items())
+
+        self._close(handle)
+        return vhd_info
+
+    def _get_vhd_info_member(self, vhd_file, info_member):
+        virt_disk_info = Win32_GET_VIRTUAL_DISK_INFO_PARAMETERS()
+        virt_disk_info.VERSION = ctypes.c_uint(info_member)
+
+        infoSize = ctypes.sizeof(virt_disk_info)
+
+        virtdisk.GetVirtualDiskInformation.restype = wintypes.DWORD
+
+        ret_val = virtdisk.GetVirtualDiskInformation(
+            vhd_file, ctypes.byref(ctypes.c_ulong(infoSize)),
+            ctypes.byref(virt_disk_info), 0)
+
+        if (ret_val and info_member !=
+                GET_VIRTUAL_DISK_INFO_PARENT_LOCATION):
+            # Note(lpetrut): If the vhd has no parent image, this will
+            # return an non-zero exit code. No need to raise an exception
+            # in this case.
+            self._close(vhd_file)
+            raise exception.VolumeBackendAPIException(
+                "Error getting vhd info. Error code: %s" % ret_val)
+
+        return self._parse_vhd_info(virt_disk_info, info_member)
+
+    def _parse_vhd_info(self, virt_disk_info, info_member):
+        vhd_info = {}
+        vhd_info_member = self._vhd_info_members[info_member]
+        info = getattr(virt_disk_info.VhdInfo, vhd_info_member)
+
+        if hasattr(info, '_fields_'):
+            for field in info._fields_:
+                vhd_info[field[0]] = getattr(info, field[0])
+        else:
+            vhd_info[vhd_info_member] = info
+
+        return vhd_info
+
+    def get_vhd_size(self, vhd_path):
+        """Returns a dict containing the virtual size, physical size,
+        block size and sector size of the vhd.
+        """
+        size = self.get_vhd_info(vhd_path,
+                                 [GET_VIRTUAL_DISK_INFO_SIZE])
+        return size
+
+    def get_vhd_parent_path(self, vhd_path):
+        vhd_info = self.get_vhd_info(vhd_path,
+                                     [GET_VIRTUAL_DISK_INFO_PARENT_LOCATION])
+        parent_path = vhd_info['ParentLocationBuffer']
+
+        if len(parent_path) > 0:
+            return parent_path
+        return None
+
+    def create_dynamic_vhd(self, path, max_internal_size):
+        self._create_vhd(path,
+                         constants.VHD_TYPE_DYNAMIC,
+                         max_internal_size=max_internal_size)
+
+    def convert_vhd(self, src, dest,
+                    vhd_type=constants.VHD_TYPE_DYNAMIC):
+        self._create_vhd(dest, vhd_type, src_path=src)
+
+    def create_differencing_vhd(self, path, parent_path):
+        self._create_vhd(path,
+                         constants.VHD_TYPE_DIFFERENCING,
+                         parent_path=parent_path)
+
+    def reconnect_parent(self, child_path, parent_path):
+        open_params = Win32_OPEN_VIRTUAL_DISK_PARAMETERS_V2()
+        open_params.Version = OPEN_VIRTUAL_DISK_VERSION_2
+        open_params.GetInfoOnly = False
+
+        handle = self._open(
+            child_path,
+            open_flag=OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS,
+            open_access_mask=VIRTUAL_DISK_ACCESS_NONE,
+            open_params=ctypes.byref(open_params))
+
+        params = Win32_SET_VIRTUAL_DISK_INFO_PARAMETERS()
+        params.Version = SET_VIRTUAL_DISK_INFO_PARENT_PATH
+        params.ParentFilePath = parent_path
+
+        ret_val = virtdisk.SetVirtualDiskInformation(
+            handle,
+            ctypes.byref(params))
+        self._close(handle)
+
+        if ret_val:
+            raise exception.VolumeBackendAPIException(
+                _("Virtual disk reconnect failed with error: %s") % ret_val)