Add Windows SMB Volume Driver

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
This commit is contained in:
Lucian Petrut
2014-07-10 16:36:19 +03:00
committed by Petrut Lucian
parent 240b0c02df
commit 62a4bd270a
7 changed files with 1338 additions and 55 deletions

View File

@@ -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)

View 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)
else:
self._vhdutils.convert_vhd(self._FAKE_VHD_PATH,
self._vhdutils._create_vhd,
self._FAKE_DEST_PATH,
self._FAKE_TYPE)
constants.VHD_TYPE_DYNAMIC,
src_path=src_path,
max_internal_size=max_internal_size,
parent_path=parent_path)
else:
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_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)
else:
self._vhdutils._open(self._FAKE_DEVICE_ID,
self._FAKE_VHD_PATH)
else:
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,
fake_access_mask, fake_open_flag, fake_params,
vhdutils.ctypes.byref(vhdutils.wintypes.HANDLE()))
self.assertEqual(self._FAKE_DEVICE_ID, fake_vst.DeviceId)
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)

View File

@@ -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)

View File

@@ -14,3 +14,4 @@
VHD_TYPE_FIXED = 2
VHD_TYPE_DYNAMIC = 3
VHD_TYPE_DIFFERENCING = 4

View File

@@ -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

View File

@@ -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)

View 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.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
params.ResiliencyGuid = Win32_GUID()
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)