Cosmin Poieana 9ed4705fd6 Ironic config drive support
Search through partitions (containing raw ISO bytes) and volumes when
looking for a configuration drive. This commit implies the following:
    1. New config options are used for choosing the possible config drive
       paths (`config_drive_locations`) and the types the service will search
       for (`config_drive_types`). The old options are still available and
       marked as deprecated.
    2. The configdrive plugin was intensively refactored and size computation,
       parsing and ISO extraction bugs were fixed. The plugin will search in
       locations like cdrom, hard disks or partitions for metadata content or
       raw ISO bytes. Also, is using the `disk` windows utility for reading
       disks and listing partitions.
    3. A new method, `get_volumes`, was added in osutils
       for listing all the volumes.
    4. Removed dead code virtual_disk.py and disk.py (physical_disk.py)
       was remade from scratch.
    5. The ability to handle partitions within a disk for reading purposes
       and related bugs fixed:
        a. Wrong INVALID_HANDLE_VALUE (-1 in Python isn't the unsigned -1 of C)
        b. Erroneous geometry computations in Py3 ("/" lead to float)
        c. Comparing string with bytes in Py3
        d. High risk of IndexErrors because of the insufficient buffer reads
           relying on standard block sector sizes.

Change-Id: Ic3a5ef1ee81c694e41fc7a22abe63b0154f51065
2015-10-10 17:57:22 +03:00

335 lines
12 KiB
Python

# 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 unittest
try:
import unittest.mock as mock
except ImportError:
import mock
from cloudbaseinit import exception
from cloudbaseinit.tests import testutils
class BaseTestDevice(unittest.TestCase):
def setUp(self):
self._ctypes_mock = mock.MagicMock()
_module_patcher = mock.patch.dict(
'sys.modules',
{'ctypes': self._ctypes_mock,
'ctypes.windll': mock.MagicMock(),
'ctypes.wintypes': mock.MagicMock(),
'winioctlcon': mock.MagicMock()})
_module_patcher.start()
self.addCleanup(_module_patcher.stop)
self.disk = importlib.import_module(
"cloudbaseinit.utils.windows.disk")
self.mock_dword = mock.Mock()
self._ctypes_mock.wintypes.DWORD = self.mock_dword
self.disk.kernel32 = mock.MagicMock()
self.geom = mock.Mock()
self.geom.MediaType = self.disk.Win32_DiskGeometry.FixedMedia = 12
self.geom.Cylinders = 2
self.geom.TracksPerCylinder = 5
self.geom.SectorsPerTrack = 512
self.geom.BytesPerSector = 512
class TestBaseDevice(BaseTestDevice, testutils.CloudbaseInitTestBase):
def setUp(self):
super(TestBaseDevice, self).setUp()
class FinalBaseDevice(self.disk.BaseDevice):
def size(self):
return 0
self.fake_path = mock.sentinel.fake_path
self._device_class = FinalBaseDevice(
path=self.fake_path)
self._device_class._sector_size = self.geom.BytesPerSector
self._device_class._disk_size = 2 * 5 * 512 * 512
def _test_open(self, exc):
if exc:
self.disk.kernel32.CreateFileW.return_value = \
self._device_class.INVALID_HANDLE_VALUE
with self.assert_raises_windows_message(
"Cannot open file: %r", 100):
self._device_class.open()
else:
self._device_class.open()
self.disk.kernel32.CreateFileW.assert_called_once_with(
self._ctypes_mock.c_wchar_p.return_value,
self._device_class.GENERIC_READ,
self._device_class.FILE_SHARE_READ,
0, self._device_class.OPEN_EXISTING,
self._device_class.FILE_ATTRIBUTE_READONLY, 0
)
self._ctypes_mock.c_wchar_p.assert_called_once_with(self.fake_path)
self.assertEqual(
self.disk.kernel32.CreateFileW.return_value,
self._device_class._handle)
def test_open(self):
self._test_open(exc=False)
def test_open_exception(self):
self._test_open(exc=True)
def test_close(self):
self._device_class._handle = mock.sentinel._handle
self._device_class.close()
self.disk.kernel32.CloseHandle.assert_called_once_with(
mock.sentinel._handle)
self.assertEqual(None, self._device_class._handle)
def _test_get_geometry(self, ret_val, last_error=None):
mock_disk_geom = self.disk.Win32_DiskGeometry
mock_disk_geom.side_effect = None
mock_disk_geom.return_value = self.geom
mock_DeviceIoControl = self.disk.kernel32.DeviceIoControl
expect_byref = [mock.call(self.geom),
mock.call(
self.mock_dword.return_value)]
self.disk.kernel32.DeviceIoControl.return_value = ret_val
if not ret_val:
with self.assert_raises_windows_message(
"Cannot get disk geometry: %r", last_error):
self._device_class._get_geometry()
else:
response = self._device_class._get_geometry()
self.mock_dword.assert_called_once_with()
mock_DeviceIoControl.assert_called_once_with(
self._device_class._handle,
self.disk.winioctlcon.IOCTL_DISK_GET_DRIVE_GEOMETRY, 0, 0,
self._ctypes_mock.byref.return_value,
self._ctypes_mock.sizeof.return_value,
self._ctypes_mock.byref.return_value, 0)
self.assertEqual(expect_byref,
self._ctypes_mock.byref.call_args_list)
self.assertEqual((self._device_class._sector_size,
self._device_class._disk_size, True),
response)
def test_get_geometry(self):
self._test_get_geometry(ret_val=mock.sentinel.ret_val)
def test_get_geometry_exception(self):
self._test_get_geometry(ret_val=0, last_error=100)
def _test__seek(self, exc):
expect_DWORD = [mock.call(0), mock.call(1)]
if exc:
self.disk.kernel32.SetFilePointer.return_value = \
self._device_class.INVALID_SET_FILE_POINTER
with self.assert_raises_windows_message(
"Seek error: %r", 100):
self._device_class._seek(1)
else:
self._device_class._seek(1)
self.disk.kernel32.SetFilePointer.assert_called_once_with(
self._device_class._handle,
self.mock_dword.return_value,
self._ctypes_mock.byref.return_value,
self._device_class.FILE_BEGIN)
self._ctypes_mock.byref.assert_called_once_with(
self.mock_dword.return_value)
self.assertEqual(expect_DWORD,
self.mock_dword.call_args_list)
def test__seek(self):
self._test__seek(exc=False)
def test__seek_exception(self):
self._test__seek(exc=True)
def test_seek(self):
offset = self._device_class.seek(1025)
self.assertEqual(1024, offset)
def _test__read(self, ret_val, last_error=None):
bytes_to_read = mock.sentinel.bytes_to_read
self.disk.kernel32.ReadFile.return_value = ret_val
if not ret_val:
with self.assert_raises_windows_message(
"Read exception: %r", last_error):
self._device_class._read(bytes_to_read)
else:
response = self._device_class._read(bytes_to_read)
mock_buffer = self._ctypes_mock.create_string_buffer
mock_buffer.assert_called_once_with(bytes_to_read)
self.mock_dword.assert_called_once_with()
self.disk.kernel32.ReadFile.assert_called_once_with(
self._device_class._handle,
mock_buffer.return_value,
bytes_to_read, self._ctypes_mock.byref.return_value, 0)
self._ctypes_mock.byref.assert_called_once_with(
self.mock_dword.return_value)
self.assertEqual(
mock_buffer.return_value.raw[
:self.mock_dword.return_value.value], response)
def test__read(self):
self._test__read(ret_val=mock.sentinel.ret_val)
def test__read_exception(self):
self._test__read(ret_val=None, last_error=100)
def test_read(self):
_read_func = mock.Mock()
mock_content = mock.MagicMock()
_read_func.return_value = mock_content
self._device_class._read = _read_func
response = self._device_class.read(512, 10)
self._device_class._read.assert_called_once_with(1024)
self.assertEqual(response, mock_content[10, 522])
class TestDisk(BaseTestDevice, testutils.CloudbaseInitTestBase):
def setUp(self):
super(TestDisk, self).setUp()
self.fake_disk_path = mock.sentinel.fake_disk_path
self._disk_class = self.disk.Disk(
path=self.fake_disk_path)
self._disk_class._disk_size = 2 * 5 * 512 * 512
@mock.patch("cloudbaseinit.utils.windows.disk"
".Win32_DRIVE_LAYOUT_INFORMATION_EX")
def _test_get_layout(self, mock_layout_struct, fail=False):
mock_layout = mock.Mock()
mock_layout_struct.return_value = mock_layout
mock_devio = self.disk.kernel32.DeviceIoControl
if fail:
mock_devio.return_value = 0
with self.assert_raises_windows_message(
"Cannot get disk layout: %r", 100):
self._disk_class._get_layout()
return
mock_devio.return_value = 1
response = self._disk_class._get_layout()
mock_devio.assert_called_once_with(
self._disk_class._handle,
self.disk.winioctlcon.IOCTL_DISK_GET_DRIVE_LAYOUT_EX,
0,
0,
self._ctypes_mock.byref(mock_layout),
self._ctypes_mock.sizeof(mock_layout),
self._ctypes_mock.byref(self.mock_dword.return_value),
0)
self.assertEqual(mock_layout, response)
def test_get_layout_fail(self):
self._test_get_layout(fail=True)
def test_get_layout(self):
self._test_get_layout()
def _test_get_partition_indexes(self, fail=False, gpt=True):
layout = mock.MagicMock()
if fail:
with self.assertRaises(exception.CloudbaseInitException):
self._disk_class._get_partition_indexes(layout)
return
part_style = (self._disk_class.PARTITION_STYLE_GPT if gpt
else self._disk_class.PARTITION_STYLE_MBR)
layout.PartitionStyle = part_style
count = 8
layout.PartitionCount = count
if gpt:
expected = list(range(count))
else:
layout.PartitionEntry = [mock.Mock() for _ in range(count)]
layout.PartitionEntry[-1].Mbr.PartitionType = \
self._disk_class.PARTITION_ENTRY_UNUSED
expected = list(range(count - 1))
response = self._disk_class._get_partition_indexes(layout)
self.assertEqual(expected, response)
def test_get_partition_indexes_fail(self):
self._test_get_partition_indexes(fail=True)
def test_get_partition_indexes_gpt(self):
self._test_get_partition_indexes()
def test_get_partition_indexes_mbr(self):
self._test_get_partition_indexes(gpt=False)
@mock.patch("cloudbaseinit.utils.windows.disk"
".Partition")
@mock.patch("cloudbaseinit.utils.windows.disk"
".Disk._get_partition_indexes")
@mock.patch("cloudbaseinit.utils.windows.disk"
".Disk._get_layout")
def test_partitions(self, mock_get_layout, mock_get_partition_indexes,
mock_partition):
size = 512
layout = mock.MagicMock()
layout.PartitionEntry[0].PartitionLength = size
indexes = [0, 1, 2]
mock_get_layout.return_value = layout
mock_get_partition_indexes.return_value = indexes
self._disk_class._path = r"\\?\GLOBALROOT\Device\Harddisk0"
response = self._disk_class.partitions()
mock_get_layout.assert_called_once_with()
mock_get_partition_indexes.assert_called_once_with(layout)
paths = [r"\\?\GLOBALROOT\Device\Harddisk{}\Partition{}".format(
0, idx + 1) for idx in indexes]
calls = [mock.call(path, size) for path in paths]
mock_partition.assert_has_calls(calls)
expected = [mock_partition(path, size) for path in paths]
self.assertEqual(expected, response)
class TestPartition(unittest.TestCase):
def setUp(self):
_module_patcher = mock.patch.dict(
'sys.modules',
{'ctypes': mock.MagicMock(),
'winioctlcon': mock.MagicMock()})
_module_patcher.start()
self.addCleanup(_module_patcher.stop)
self.disk = importlib.import_module(
"cloudbaseinit.utils.windows.disk")
def test_size(self):
size = mock.sentinel.size
partition = self.disk.Partition(mock.Mock(), size)
self.assertEqual(size, partition.size)