Merge "Implement VHD attach/detach"
This commit is contained in:
commit
4e24801719
|
@ -279,3 +279,11 @@ class SCSIPageParsingError(Invalid):
|
|||
class SCSIIdDescriptorParsingError(Invalid):
|
||||
msg_fmt = _("Parsing SCSI identification descriptor failed. "
|
||||
"Reason: %(reason)s.")
|
||||
|
||||
|
||||
class ResourceUpdateError(OSWinException):
|
||||
msg_fmt = _("Failed to update the specified resource.")
|
||||
|
||||
|
||||
class DiskUpdateError(OSWinException):
|
||||
msg_fmt = _("Failed to update the specified disk.")
|
||||
|
|
|
@ -256,6 +256,15 @@ class DiskUtilsTestCase(test_base.OsWinBaseTestCase):
|
|||
self._test_get_disk_capacity(
|
||||
raised_exc=exceptions.Win32Exception)
|
||||
|
||||
@mock.patch.object(diskutils.DiskUtils, '_get_disk_by_number')
|
||||
def test_get_disk_size(self, mock_get_disk):
|
||||
disk_size = self._diskutils.get_disk_size(
|
||||
mock.sentinel.disk_number)
|
||||
|
||||
self.assertEqual(mock_get_disk.return_value.Size, disk_size)
|
||||
|
||||
mock_get_disk.assert_called_once_with(mock.sentinel.disk_number)
|
||||
|
||||
def test_parse_scsi_id_desc(self):
|
||||
vpd_str = ('008300240103001060002AC00000000000000EA0'
|
||||
'0000869902140004746573740115000400000001')
|
||||
|
@ -384,3 +393,57 @@ class DiskUtilsTestCase(test_base.OsWinBaseTestCase):
|
|||
setting_cls = self._diskutils._conn_storage.MSFT_StorageSetting
|
||||
setting_cls.Set.assert_called_once_with(
|
||||
NewDiskPolicy=mock.sentinel.policy)
|
||||
|
||||
@mock.patch.object(diskutils.DiskUtils, '_get_disk_by_number')
|
||||
@ddt.data(0, 1)
|
||||
def test_set_disk_online(self, err_code, mock_get_disk):
|
||||
mock_disk = mock_get_disk.return_value
|
||||
mock_disk.Online.return_value = (mock.sentinel.ext_err_info,
|
||||
err_code)
|
||||
|
||||
if err_code:
|
||||
self.assertRaises(exceptions.DiskUpdateError,
|
||||
self._diskutils.set_disk_online,
|
||||
mock.sentinel.disk_number)
|
||||
else:
|
||||
self._diskutils.set_disk_online(mock.sentinel.disk_number)
|
||||
|
||||
mock_disk.Online.assert_called_once_with()
|
||||
mock_get_disk.assert_called_once_with(mock.sentinel.disk_number)
|
||||
|
||||
@mock.patch.object(diskutils.DiskUtils, '_get_disk_by_number')
|
||||
@ddt.data(0, 1)
|
||||
def test_set_disk_offline(self, err_code, mock_get_disk):
|
||||
mock_disk = mock_get_disk.return_value
|
||||
mock_disk.Offline.return_value = (mock.sentinel.ext_err_info,
|
||||
err_code)
|
||||
|
||||
if err_code:
|
||||
self.assertRaises(exceptions.DiskUpdateError,
|
||||
self._diskutils.set_disk_offline,
|
||||
mock.sentinel.disk_number)
|
||||
else:
|
||||
self._diskutils.set_disk_offline(mock.sentinel.disk_number)
|
||||
|
||||
mock_disk.Offline.assert_called_once_with()
|
||||
mock_get_disk.assert_called_once_with(mock.sentinel.disk_number)
|
||||
|
||||
@mock.patch.object(diskutils.DiskUtils, '_get_disk_by_number')
|
||||
@ddt.data(0, 1)
|
||||
def test_set_disk_readonly(self, err_code, mock_get_disk):
|
||||
mock_disk = mock_get_disk.return_value
|
||||
mock_disk.SetAttributes.return_value = (mock.sentinel.ext_err_info,
|
||||
err_code)
|
||||
|
||||
if err_code:
|
||||
self.assertRaises(exceptions.DiskUpdateError,
|
||||
self._diskutils.set_disk_readonly_status,
|
||||
mock.sentinel.disk_number,
|
||||
read_only=True)
|
||||
else:
|
||||
self._diskutils.set_disk_readonly_status(
|
||||
mock.sentinel.disk_number,
|
||||
read_only=True)
|
||||
|
||||
mock_disk.SetAttributes.assert_called_once_with(IsReadOnly=True)
|
||||
mock_get_disk.assert_called_once_with(mock.sentinel.disk_number)
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from oslotest import base
|
||||
import six
|
||||
|
@ -22,8 +24,10 @@ from os_win import constants
|
|||
from os_win import exceptions
|
||||
from os_win.utils.storage.virtdisk import vhdutils
|
||||
from os_win.utils.winapi import constants as w_const
|
||||
from os_win.utils.winapi import wintypes
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class VHDUtilsTestCase(base.BaseTestCase):
|
||||
"""Unit tests for the Hyper-V VHDUtils class."""
|
||||
|
||||
|
@ -51,8 +55,12 @@ class VHDUtilsTestCase(base.BaseTestCase):
|
|||
self._ctypes.c_wchar_p = lambda x: (x, "c_wchar_p")
|
||||
self._ctypes.c_ulong = lambda x: (x, "c_ulong")
|
||||
|
||||
self._ctypes_patcher = mock.patch.object(
|
||||
vhdutils, 'ctypes', self._ctypes)
|
||||
self._ctypes_patcher.start()
|
||||
|
||||
mock.patch.multiple(vhdutils,
|
||||
ctypes=self._ctypes, kernel32=mock.DEFAULT,
|
||||
kernel32=mock.DEFAULT,
|
||||
wintypes=mock.DEFAULT, virtdisk=mock.DEFAULT,
|
||||
vdisk_struct=self._vdisk_struct,
|
||||
create=True).start()
|
||||
|
@ -116,8 +124,8 @@ class VHDUtilsTestCase(base.BaseTestCase):
|
|||
**self._run_args)
|
||||
|
||||
def test_close(self):
|
||||
self._vhdutils._close(mock.sentinel.handle)
|
||||
vhdutils.kernel32.CloseHandle.assert_called_once_with(
|
||||
self._vhdutils.close(mock.sentinel.handle)
|
||||
self._mock_close.assert_called_once_with(
|
||||
mock.sentinel.handle)
|
||||
|
||||
@mock.patch.object(vhdutils.VHDUtils, '_get_vhd_device_id')
|
||||
|
@ -760,3 +768,99 @@ class VHDUtilsTestCase(base.BaseTestCase):
|
|||
def test_get_best_supported_vhd_format(self):
|
||||
fmt = self._vhdutils.get_best_supported_vhd_format()
|
||||
self.assertEqual(constants.DISK_FORMAT_VHDX, fmt)
|
||||
|
||||
@ddt.data({},
|
||||
{'read_only': False, 'detach_on_handle_close': True})
|
||||
@ddt.unpack
|
||||
@mock.patch.object(vhdutils.VHDUtils, '_open')
|
||||
def test_attach_virtual_disk(self, mock_open, read_only=True,
|
||||
detach_on_handle_close=False):
|
||||
ret_val = self._vhdutils.attach_virtual_disk(
|
||||
mock.sentinel.vhd_path,
|
||||
read_only, detach_on_handle_close)
|
||||
|
||||
handle = mock_open.return_value
|
||||
self.assertEqual(handle
|
||||
if detach_on_handle_close else None,
|
||||
ret_val)
|
||||
|
||||
exp_access_mask = (w_const.VIRTUAL_DISK_ACCESS_ATTACH_RO
|
||||
if read_only
|
||||
else w_const.VIRTUAL_DISK_ACCESS_ATTACH_RW)
|
||||
mock_open.assert_called_once_with(mock.sentinel.vhd_path,
|
||||
open_access_mask=exp_access_mask)
|
||||
|
||||
self._mock_run.assert_called_once_with(
|
||||
vhdutils.virtdisk.AttachVirtualDisk,
|
||||
handle,
|
||||
None,
|
||||
mock.ANY,
|
||||
0, None, None,
|
||||
**self._run_args)
|
||||
|
||||
if not detach_on_handle_close:
|
||||
self._mock_close.assert_called_once_with(handle)
|
||||
else:
|
||||
self._mock_close.assert_not_called()
|
||||
|
||||
mock_run_args = self._mock_run.call_args_list[0][0]
|
||||
attach_flag = mock_run_args[3]
|
||||
|
||||
self.assertEqual(
|
||||
read_only,
|
||||
bool(attach_flag & w_const.ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY))
|
||||
self.assertEqual(
|
||||
not detach_on_handle_close,
|
||||
bool(attach_flag &
|
||||
w_const.ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME))
|
||||
|
||||
@mock.patch.object(vhdutils.VHDUtils, '_open')
|
||||
def test_detach_virtual_disk(self, mock_open):
|
||||
self._mock_run.return_value = w_const.ERROR_NOT_READY
|
||||
|
||||
self._vhdutils.detach_virtual_disk(mock.sentinel.vhd_path)
|
||||
|
||||
mock_open.assert_called_once_with(
|
||||
mock.sentinel.vhd_path,
|
||||
open_access_mask=w_const.VIRTUAL_DISK_ACCESS_DETACH)
|
||||
|
||||
self._mock_run.assert_called_once_with(
|
||||
vhdutils.virtdisk.DetachVirtualDisk,
|
||||
mock_open.return_value,
|
||||
0, 0,
|
||||
ignored_error_codes=[w_const.ERROR_NOT_READY],
|
||||
**self._run_args)
|
||||
self._mock_close.assert_called_once_with(mock_open.return_value)
|
||||
|
||||
@mock.patch.object(vhdutils.VHDUtils, '_open')
|
||||
def test_get_virtual_disk_physical_path(self, mock_open):
|
||||
self._ctypes_patcher.stop()
|
||||
vhdutils.wintypes = wintypes
|
||||
|
||||
fake_drive_path = r'\\.\PhysicialDrive5'
|
||||
|
||||
def fake_run(func, handle, disk_path_sz_p, disk_path, **kwargs):
|
||||
disk_path_sz = ctypes.cast(
|
||||
disk_path_sz_p, wintypes.PULONG).contents.value
|
||||
self.assertEqual(w_const.MAX_PATH, disk_path_sz)
|
||||
|
||||
disk_path.value = fake_drive_path
|
||||
|
||||
self._mock_run.side_effect = fake_run
|
||||
|
||||
ret_val = self._vhdutils.get_virtual_disk_physical_path(
|
||||
mock.sentinel.vhd_path)
|
||||
|
||||
self.assertEqual(fake_drive_path, ret_val)
|
||||
mock_open.assert_called_once_with(
|
||||
mock.sentinel.vhd_path,
|
||||
open_flag=w_const.OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS,
|
||||
open_access_mask=(w_const.VIRTUAL_DISK_ACCESS_GET_INFO |
|
||||
w_const.VIRTUAL_DISK_ACCESS_DETACH))
|
||||
|
||||
self._mock_run.assert_called_once_with(
|
||||
vhdutils.virtdisk.GetVirtualDiskPhysicalPath,
|
||||
mock_open.return_value,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
**self._run_args)
|
||||
|
|
|
@ -171,6 +171,7 @@ class DiskUtils(baseutils.BaseUtils):
|
|||
LOG.debug("Finished rescanning disks.")
|
||||
|
||||
def get_disk_capacity(self, path, ignore_errors=False):
|
||||
"""Returns total/free space for a given directory."""
|
||||
norm_path = os.path.abspath(path)
|
||||
|
||||
total_bytes = ctypes.c_ulonglong(0)
|
||||
|
@ -195,6 +196,11 @@ class DiskUtils(baseutils.BaseUtils):
|
|||
else:
|
||||
raise exc
|
||||
|
||||
def get_disk_size(self, disk_number):
|
||||
"""Returns the disk size, given a physical disk number."""
|
||||
disk = self._get_disk_by_number(disk_number)
|
||||
return disk.Size
|
||||
|
||||
def _parse_scsi_page_83(self, buff,
|
||||
select_supported_identifiers=False):
|
||||
"""Parse SCSI Device Identification VPD (page 0x83 data).
|
||||
|
@ -316,3 +322,35 @@ class DiskUtils(baseutils.BaseUtils):
|
|||
"""
|
||||
self._conn_storage.MSFT_StorageSetting.Set(
|
||||
NewDiskPolicy=policy)
|
||||
|
||||
def set_disk_online(self, disk_number):
|
||||
disk = self._get_disk_by_number(disk_number)
|
||||
err_code = disk.Online()[1]
|
||||
if err_code:
|
||||
err_msg = (_("Failed to bring disk '%(disk_number)s' online. "
|
||||
"Error code: %(err_code)s.") %
|
||||
dict(disk_number=disk_number,
|
||||
err_code=err_code))
|
||||
raise exceptions.DiskUpdateError(message=err_msg)
|
||||
|
||||
def set_disk_offline(self, disk_number):
|
||||
disk = self._get_disk_by_number(disk_number)
|
||||
err_code = disk.Offline()[1]
|
||||
if err_code:
|
||||
err_msg = (_("Failed to bring disk '%(disk_number)s' offline. "
|
||||
"Error code: %(err_code)s.") %
|
||||
dict(disk_number=disk_number,
|
||||
err_code=err_code))
|
||||
raise exceptions.DiskUpdateError(message=err_msg)
|
||||
|
||||
def set_disk_readonly_status(self, disk_number, read_only):
|
||||
disk = self._get_disk_by_number(disk_number)
|
||||
err_code = disk.SetAttributes(IsReadOnly=bool(read_only))[1]
|
||||
if err_code:
|
||||
err_msg = (_("Failed to set disk '%(disk_number)s' read-only "
|
||||
"status to '%(read_only)s'. "
|
||||
"Error code: %(err_code)s.") %
|
||||
dict(disk_number=disk_number,
|
||||
err_code=err_code,
|
||||
read_only=bool(read_only)))
|
||||
raise exceptions.DiskUpdateError(message=err_msg)
|
||||
|
|
|
@ -126,8 +126,8 @@ class VHDUtils(object):
|
|||
ctypes.byref(handle))
|
||||
return handle
|
||||
|
||||
def _close(self, handle):
|
||||
kernel32.CloseHandle(handle)
|
||||
def close(self, handle):
|
||||
self._win32_utils.close_handle(handle)
|
||||
|
||||
def create_vhd(self, new_vhd_path, new_vhd_type, src_path=None,
|
||||
max_internal_size=0, parent_path=None):
|
||||
|
@ -546,3 +546,86 @@ class VHDUtils(object):
|
|||
|
||||
os.unlink(vhd_path)
|
||||
os.rename(tmp_path, vhd_path)
|
||||
|
||||
def attach_virtual_disk(self, vhd_path, read_only=True,
|
||||
detach_on_handle_close=False):
|
||||
"""Attach a virtual disk image.
|
||||
|
||||
:param vhd_path: the path of the image to attach
|
||||
:param read_only: (bool) attach the image in read only mode
|
||||
:parma detach_on_handle_close: if set, the image will automatically be
|
||||
detached when the last image handle is
|
||||
closed.
|
||||
:returns: if 'detach_on_handle_close' is set, it returns a virtual
|
||||
disk image handle that may be closed using the
|
||||
'close' method of this class.
|
||||
"""
|
||||
open_access_mask = (w_const.VIRTUAL_DISK_ACCESS_ATTACH_RO
|
||||
if read_only
|
||||
else w_const.VIRTUAL_DISK_ACCESS_ATTACH_RW)
|
||||
attach_virtual_disk_flag = 0
|
||||
if not detach_on_handle_close:
|
||||
attach_virtual_disk_flag |= (
|
||||
w_const.ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME)
|
||||
if read_only:
|
||||
attach_virtual_disk_flag |= (
|
||||
w_const.ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY)
|
||||
|
||||
handle = self._open(
|
||||
vhd_path,
|
||||
open_access_mask=open_access_mask)
|
||||
|
||||
self._run_and_check_output(
|
||||
virtdisk.AttachVirtualDisk,
|
||||
handle,
|
||||
None, # security descriptor
|
||||
attach_virtual_disk_flag,
|
||||
0, # provider specific flags
|
||||
None, # attach parameters
|
||||
None, # overlapped structure
|
||||
cleanup_handle=handle if not detach_on_handle_close else None)
|
||||
|
||||
if detach_on_handle_close:
|
||||
return handle
|
||||
|
||||
def detach_virtual_disk(self, vhd_path):
|
||||
open_access_mask = w_const.VIRTUAL_DISK_ACCESS_DETACH
|
||||
handle = self._open(vhd_path, open_access_mask=open_access_mask)
|
||||
|
||||
ret_val = self._run_and_check_output(
|
||||
virtdisk.DetachVirtualDisk,
|
||||
handle,
|
||||
0, # detach flags
|
||||
0, # provider specific flags
|
||||
ignored_error_codes=[w_const.ERROR_NOT_READY],
|
||||
cleanup_handle=handle)
|
||||
|
||||
if ret_val == w_const.ERROR_NOT_READY:
|
||||
LOG.debug("Image %s was not attached.", vhd_path)
|
||||
|
||||
def get_virtual_disk_physical_path(self, vhd_path):
|
||||
"""Returns the physical disk path for an attached disk image.
|
||||
|
||||
:param vhd_path: an attached disk image path.
|
||||
:returns: the mount path of the specified image, in the form of
|
||||
\\.\PhysicalDriveX.
|
||||
"""
|
||||
|
||||
open_flag = w_const.OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS
|
||||
open_access_mask = (w_const.VIRTUAL_DISK_ACCESS_GET_INFO |
|
||||
w_const.VIRTUAL_DISK_ACCESS_DETACH)
|
||||
handle = self._open(
|
||||
vhd_path,
|
||||
open_flag=open_flag,
|
||||
open_access_mask=open_access_mask)
|
||||
|
||||
disk_path = (ctypes.c_wchar * w_const.MAX_PATH)()
|
||||
disk_path_sz = wintypes.ULONG(w_const.MAX_PATH)
|
||||
self._run_and_check_output(
|
||||
virtdisk.GetVirtualDiskPhysicalPath,
|
||||
handle,
|
||||
ctypes.byref(disk_path_sz),
|
||||
disk_path,
|
||||
cleanup_handle=handle)
|
||||
|
||||
return disk_path.value
|
||||
|
|
|
@ -19,6 +19,7 @@ from os_win.utils.winapi import wintypes
|
|||
# ----------
|
||||
# winerror.h
|
||||
ERROR_INVALID_HANDLE = 6
|
||||
ERROR_NOT_READY = 21
|
||||
ERROR_INSUFFICIENT_BUFFER = 122
|
||||
ERROR_DIR_IS_NOT_EMPTY = 145
|
||||
ERROR_PIPE_BUSY = 231
|
||||
|
@ -256,6 +257,11 @@ VIRTUAL_DISK_ACCESS_ALL = 0x003f0000
|
|||
VIRTUAL_DISK_ACCESS_CREATE = 0x00100000
|
||||
VIRTUAL_DISK_ACCESS_GET_INFO = 0x80000
|
||||
VIRTUAL_DISK_ACCESS_DETACH = 0x00040000
|
||||
VIRTUAL_DISK_ACCESS_ATTACH_RO = 0x00010000
|
||||
VIRTUAL_DISK_ACCESS_ATTACH_RW = 0x00020000
|
||||
|
||||
ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY = 0x00000001
|
||||
ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME = 0x00000004
|
||||
|
||||
OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS = 1
|
||||
OPEN_VIRTUAL_DISK_VERSION_1 = 1
|
||||
|
|
|
@ -246,3 +246,27 @@ def register():
|
|||
PSET_VIRTUAL_DISK_INFO
|
||||
]
|
||||
lib_handle.SetVirtualDiskInformation.restype = wintypes.DWORD
|
||||
|
||||
lib_handle.AttachVirtualDisk.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
wintypes.PVOID,
|
||||
wintypes.INT,
|
||||
wintypes.ULONG,
|
||||
wintypes.PVOID,
|
||||
wintypes.LPOVERLAPPED
|
||||
]
|
||||
lib_handle.AttachVirtualDisk.restype = wintypes.DWORD
|
||||
|
||||
lib_handle.DetachVirtualDisk.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
wintypes.INT,
|
||||
wintypes.ULONG
|
||||
]
|
||||
lib_handle.DetachVirtualDisk.restype = wintypes.DWORD
|
||||
|
||||
lib_handle.GetVirtualDiskPhysicalPath.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
wintypes.PULONG,
|
||||
wintypes.PWSTR
|
||||
]
|
||||
lib_handle.GetVirtualDiskPhysicalPath.restype = wintypes.DWORD
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
os-win now allows attaching/detaching VHD/x images, retrieving the
|
||||
attached disk physical paths and changing their available and read-only
|
||||
status.
|
Loading…
Reference in New Issue