Adrian Vladu 5dba5c60f6 Check all configdrive types if one errors out
By default, all config drive types are checked.
But if somehow, the implementation of each type
fails, the metadata service errors out and does not
check for the next type.

The issue was reported here:
https://ask.cloudbase.it/question/3094/windows-server-2016-extendvolumespluginp-doesnt-work/

In that case, in the method is_vfat_drive:
  match = VOLUME_LABEL_REGEX.search(out)
  return match.group(1) in CONFIG_DRIVE_LABELS

if match value is None, the return line throws an error:
  AttributeError: 'NoneType' object has no attribute 'group'

To make sure that no other implementation will bubble up
the error, we catch the error in the config_drive metadata
service.

Catching the error will allow that the next config_drive
type will be checked for metadata.

Change-Id: I0d9967ec6a81214c7d78be667cffa4a98758587a
2019-10-28 15:54:28 +02:00

465 lines
20 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 itertools
import os
import unittest
try:
import unittest.mock as mock
except ImportError:
import mock
from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit import exception
from cloudbaseinit.tests import testutils
CONF = cloudbaseinit_conf.CONF
OPEN = mock.mock_open()
class TestWindowsConfigDriveManager(unittest.TestCase):
def setUp(self):
module_path = "cloudbaseinit.metadata.services.osconfigdrive.windows"
mock_ctypes = mock.MagicMock()
mock_ctypes.wintypes = mock.MagicMock()
self._module_patcher = mock.patch.dict(
'sys.modules',
{'disk': mock.Mock(),
'ctypes': mock_ctypes,
'winioctlcon': mock.Mock()})
self._module_patcher.start()
self.addCleanup(self._module_patcher.stop)
self.conf_module = importlib.import_module(module_path)
self.conf_module.osutils_factory = mock.Mock()
self.conf_module.disk.Disk = mock.MagicMock()
self.conf_module.tempfile = mock.Mock()
self.mock_gettempdir = self.conf_module.tempfile.gettempdir
self.mock_gettempdir.return_value = "tempdir"
self.conf_module.uuid = mock.Mock()
self.mock_uuid4 = self.conf_module.uuid.uuid4
self.mock_uuid4.return_value = "uuid"
self._config_manager = self.conf_module.WindowsConfigDriveManager()
self.addCleanup(os.rmdir, self._config_manager.target_path)
self.osutils = mock.Mock()
self._config_manager._osutils = self.osutils
self.snatcher = testutils.LogSnatcher(module_path)
@mock.patch('os.path.exists')
def _test_check_for_config_drive(self, mock_exists, exists=True,
label="config-2", fail=False):
drive = "C:\\"
self.osutils.get_volume_label.return_value = label
mock_exists.return_value = exists
with self.snatcher:
response = self._config_manager._check_for_config_drive(drive)
self.osutils.get_volume_label.assert_called_once_with(drive)
if exists and not fail:
self.assertEqual(["Config Drive found on C:\\"],
self.snatcher.output)
self.assertEqual(not fail, response)
def test_check_for_config_drive_exists(self):
self._test_check_for_config_drive()
def test_check_for_config_drive_exists_upper_label(self):
self._test_check_for_config_drive(label="CONFIG-2")
def test_check_for_config_drive_missing(self):
self._test_check_for_config_drive(exists=False, fail=True)
def test_check_for_config_drive_wrong_label(self):
self._test_check_for_config_drive(label="config-3", fail=True)
def _test_get_iso_file_size(self, fixed=True, small=False,
found_iso=True):
device = mock.Mock()
device.fixed = fixed
device.size = (self.conf_module.OFFSET_BLOCK_SIZE +
self.conf_module.PEEK_SIZE + int(not small))
iso_id = self.conf_module.ISO_ID
if not found_iso:
iso_id = b"pwned"
iso_off = self.conf_module.OFFSET_ISO_ID - 1
volume_off = self.conf_module.OFFSET_VOLUME_SIZE - 1
block_off = self.conf_module.OFFSET_BLOCK_SIZE - 1
volume_bytes = b'd\x00' # 100
block_bytes = b'\x00\x02' # 512
device.seek.side_effect = [iso_off, volume_off, block_off]
device.read.side_effect = [iso_id, volume_bytes, block_bytes]
response = self._config_manager._get_iso_file_size(device)
if not fixed or small or not found_iso:
self.assertIsNone(response)
return
seek_calls = [
mock.call(self.conf_module.OFFSET_ISO_ID),
mock.call(self.conf_module.OFFSET_VOLUME_SIZE),
mock.call(self.conf_module.OFFSET_BLOCK_SIZE)]
read_calls = [
mock.call(len(iso_id),
skip=self.conf_module.OFFSET_ISO_ID - iso_off),
mock.call(self.conf_module.PEEK_SIZE,
skip=self.conf_module.OFFSET_VOLUME_SIZE - volume_off),
mock.call(self.conf_module.PEEK_SIZE,
skip=self.conf_module.OFFSET_BLOCK_SIZE - block_off)]
device.seek.assert_has_calls(seek_calls)
device.read.assert_has_calls(read_calls)
self.assertEqual(100 * 512, response)
def test_get_iso_file_size_not_fixed(self):
self._test_get_iso_file_size(fixed=False)
def test_get_iso_file_size_small(self):
self._test_get_iso_file_size(small=True)
def test_get_iso_file_size_not_found(self):
self._test_get_iso_file_size(found_iso=False)
def test_get_iso_file_size(self):
self._test_get_iso_file_size()
@mock.patch("six.moves.builtins.open", new=OPEN)
def test_write_iso_file(self):
file_path = "fake\\path"
file_size = 100 * 512
sector_size = self.conf_module.MAX_SECTOR_SIZE
offsets = list(range(0, file_size, sector_size))
remain = file_size % sector_size
reads = ([b"\x00" * sector_size] *
(len(offsets) - int(bool(remain))) +
([b"\x00" * remain] if remain else []))
device = mock.Mock()
device_seek_calls = [mock.call(off) for off in offsets]
device_read_calls = [
mock.call(min(sector_size, file_size - off), skip=0)
for off in offsets]
stream_write_calls = [mock.call(read) for read in reads]
device.seek.side_effect = offsets
device.read.side_effect = reads
self._config_manager._write_iso_file(device, file_path, file_size)
device.seek.assert_has_calls(device_seek_calls)
device.read.assert_has_calls(device_read_calls)
OPEN.return_value.write.assert_has_calls(stream_write_calls)
def _test_extract_files_from_iso(self, exit_code):
fake_path = os.path.join('fake', 'path')
fake_target_path = os.path.join(fake_path, 'target')
self._config_manager.target_path = fake_target_path
args = [CONF.bsdtar_path, '-xf', fake_path, '-C', fake_target_path]
self.osutils.execute_process.return_value = ('fake out', 'fake err',
exit_code)
if exit_code:
self.assertRaises(exception.CloudbaseInitException,
self._config_manager._extract_files_from_iso,
fake_path)
else:
self._config_manager._extract_files_from_iso(fake_path)
self.osutils.execute_process.assert_called_once_with(args, False)
def test_extract_files_from_iso(self):
self._test_extract_files_from_iso(exit_code=0)
def test_extract_files_from_iso_fail(self):
self._test_extract_files_from_iso(exit_code=1)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager._extract_files_from_iso')
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager._write_iso_file')
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager._get_iso_file_size')
def _test_extract_iso_from_devices(self, mock_get_iso_file_size,
mock_write_iso_file,
mock_extract_files_from_iso,
found=True):
# For every device (mock) in the list of available devices:
# first - skip (no size)
# second - error (throws Exception)
# third - extract (is ok)
# fourth - unreachable (already found ok device)
size = 100 * 512
devices = [mock.MagicMock() for _ in range(4)]
devices[1].__enter__.side_effect = [Exception]
rest = [size] if found else [None]
mock_get_iso_file_size.side_effect = [None] + rest * 2
file_path = os.path.join("tempdir", "uuid.iso")
with self.snatcher:
response = self._config_manager._extract_iso_from_devices(devices)
self.mock_gettempdir.assert_called_once_with()
mock_get_iso_file_size.assert_has_calls([
mock.call(devices[0]), mock.call(devices[2])])
expected_log = [
"ISO extraction failed on %(device)s with %(error)r" %
{"device": devices[1], "error": Exception()}]
if found:
mock_write_iso_file.assert_called_once_with(devices[2],
file_path, size)
mock_extract_files_from_iso.assert_called_once_with(file_path)
expected_log.append("ISO9660 disk found on %s" % devices[2])
self.assertEqual(expected_log, self.snatcher.output)
self.assertEqual(found, response)
def test_extract_iso_from_devices_not_found(self):
self._test_extract_iso_from_devices(found=False)
def test_extract_iso_from_devices(self):
self._test_extract_iso_from_devices()
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_check_for_config_drive')
@mock.patch('shutil.copytree')
@mock.patch('os.rmdir')
def _test_get_config_drive_from_cdrom_drive(self, mock_os_rmdir,
mock_copytree,
mock_check_for_config_drive,
found=True):
drives = ["C:\\", "M:\\", "I:\\", "N:\\"]
self.osutils.get_cdrom_drives.return_value = drives
checks = [False, False, True, False]
if not found:
checks[2] = False
mock_check_for_config_drive.side_effect = checks
response = self._config_manager._get_config_drive_from_cdrom_drive()
self.osutils.get_cdrom_drives.assert_called_once_with()
idx = 3 if found else 4
check_calls = [mock.call(drive) for drive in drives[:idx]]
mock_check_for_config_drive.assert_has_calls(check_calls)
if found:
mock_os_rmdir.assert_called_once_with(
self._config_manager.target_path)
mock_copytree.assert_called_once_with(
drives[2], self._config_manager.target_path)
self.assertEqual(found, response)
def test_get_config_drive_from_cdrom_drive_not_found(self):
self._test_get_config_drive_from_cdrom_drive(found=False)
def test_get_config_drive_from_cdrom_drive(self):
self._test_get_config_drive_from_cdrom_drive()
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_extract_iso_from_devices')
@mock.patch("six.moves.builtins.map")
def test_get_config_drive_from_raw_hdd(self, mock_map,
mock_extract_iso_from_devices):
Disk = self.conf_module.disk.Disk
paths = [mock.Mock() for _ in range(3)]
self.osutils.get_physical_disks.return_value = paths
mock_extract_iso_from_devices.return_value = True
response = self._config_manager._get_config_drive_from_raw_hdd()
mock_map.assert_called_once_with(Disk, paths)
self.osutils.get_physical_disks.assert_called_once_with()
mock_extract_iso_from_devices.assert_called_once_with(
mock_map.return_value)
self.assertTrue(response)
@mock.patch('cloudbaseinit.utils.windows.vfat.copy_from_vfat_drive')
@mock.patch('cloudbaseinit.utils.windows.vfat.is_vfat_drive')
def test_get_config_drive_from_vfat(self, mock_is_vfat_drive,
mock_copy_from_vfat_drive):
self.osutils.get_physical_disks.return_value = (
mock.sentinel.drive1,
mock.sentinel.drive2,
)
mock_is_vfat_drive.side_effect = (None, True)
with testutils.LogSnatcher('cloudbaseinit.metadata.services.'
'osconfigdrive.windows') as snatcher:
response = self._config_manager._get_config_drive_from_vfat()
self.assertTrue(response)
self.osutils.get_physical_disks.assert_called_once_with()
expected_is_vfat_calls = [
mock.call(self.osutils, mock.sentinel.drive1),
mock.call(self.osutils, mock.sentinel.drive2),
]
self.assertEqual(expected_is_vfat_calls, mock_is_vfat_drive.mock_calls)
mock_copy_from_vfat_drive.assert_called_once_with(
self.osutils,
mock.sentinel.drive2,
self._config_manager.target_path)
expected_logging = [
'Config Drive found on disk %r' % mock.sentinel.drive2,
]
self.assertEqual(expected_logging, snatcher.output)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_extract_iso_from_devices')
def _test_get_config_drive_from_partition(self,
mock_extract_iso_from_devices,
found=True):
paths = [mock.Mock() for _ in range(3)]
self.osutils.get_physical_disks.return_value = paths
disks = list(map(self.conf_module.disk.Disk, paths))
mock_extract_iso_from_devices.side_effect = [False, found, found]
idx = 3 - int(found)
extract_calls = [mock.call(disk.partitions())
for disk in disks[:idx]]
response = self._config_manager._get_config_drive_from_partition()
self.osutils.get_physical_disks.assert_called_once_with()
mock_extract_iso_from_devices.assert_has_calls(extract_calls)
self.assertEqual(found, response)
def test_get_config_drive_from_partition_not_found(self):
self._test_get_config_drive_from_partition(found=False)
def test_get_config_drive_from_partition(self):
self._test_get_config_drive_from_partition()
@mock.patch('os.rmdir')
@mock.patch('shutil.copytree')
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_check_for_config_drive')
def _test_get_config_drive_from_volume(self, mock_check_for_config_drive,
mock_copytree, mock_os_rmdir,
found=True):
volumes = [mock.Mock() for _ in range(3)]
self.osutils.get_volumes.return_value = volumes
checks = [False, found, found]
mock_check_for_config_drive.side_effect = checks
idx = 3 - int(found)
check_calls = [mock.call(volume) for volume in volumes[:idx]]
response = self._config_manager._get_config_drive_from_volume()
self.osutils.get_volumes.assert_called_once_with()
mock_check_for_config_drive.assert_has_calls(check_calls)
if found:
mock_os_rmdir.assert_called_once_with(
self._config_manager.target_path)
mock_copytree.assert_called_once_with(
volumes[1], self._config_manager.target_path)
self.assertEqual(found, response)
def test_get_config_drive_from_volume_not_found(self):
self._test_get_config_drive_from_volume(found=False)
def test_get_config_drive_from_volume(self):
self._test_get_config_drive_from_volume()
def _test__get_config_drive_files(self, cd_type, cd_location,
func, found=True):
response = self._config_manager._get_config_drive_files(cd_type,
cd_location)
if found:
if func:
func.assert_called_once_with()
self.assertEqual(func.return_value, response)
else:
self.assertFalse(response)
def test__get_config_drive_files_not_found(self):
self._test__get_config_drive_files(None, None, None, found=False)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_from_cdrom_drive')
def test__get_config_drive_files_cdrom_iso(self, func):
self._test__get_config_drive_files(
"iso", "cdrom", func)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_from_cdrom_drive')
def test__get_config_drive_files_cdrom_iso_failed(self, func):
func.side_effect = Exception
self._test__get_config_drive_files(
"iso", "cdrom", func, found=False)
def test__get_config_drive_files_cdrom_vfat(self):
self._test__get_config_drive_files(
"vfat", "cdrom", None)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_from_raw_hdd')
def test__get_config_drive_files_hdd_iso(self, func):
self._test__get_config_drive_files(
"iso", "hdd", func)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_from_vfat')
def test__get_config_drive_files_hdd_vfat(self, func):
self._test__get_config_drive_files(
"vfat", "hdd", func)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_from_partition')
def test__get_config_drive_files_partition_iso(self, func):
self._test__get_config_drive_files(
"iso", "partition", func)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_from_volume')
def test__get_config_drive_files_partition_vfat(self, func):
self._test__get_config_drive_files(
"vfat", "partition", func)
@mock.patch('cloudbaseinit.metadata.services.osconfigdrive.windows.'
'WindowsConfigDriveManager.'
'_get_config_drive_files')
def _test_get_config_drive_files(self, mock_get_config_drive_files,
found=True):
check_types = ["iso", "vfat"] if found else []
check_locations = ["cdrom", "hdd", "partition"]
product = list(itertools.product(check_types, check_locations))
product_calls = [mock.call(cd_type, cd_location)
for cd_type, cd_location in product]
mock_get_config_drive_files.side_effect = \
[False] * (len(product_calls) - 1) + [True]
expected_log = ["Looking for Config Drive %(type)s in %(location)s" %
{"type": cd_type, "location": cd_location}
for cd_type, cd_location in product]
with self.snatcher:
response = self._config_manager.get_config_drive_files(
check_types, check_locations)
mock_get_config_drive_files.assert_has_calls(product_calls)
self.assertEqual(expected_log, self.snatcher.output)
self.assertEqual(found, response)
def test_get_config_drive_files_not_found(self):
self._test_get_config_drive_files(found=False)
def test_get_config_drive_files(self):
self._test_get_config_drive_files()