Merge "Implement VHD attach/detach"

This commit is contained in:
Zuul 2017-12-19 13:38:34 +00:00 committed by Gerrit Code Review
commit 4e24801719
8 changed files with 337 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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